From b2cb95829cea46b6e1baeda43e0c4fda551d3c25 Mon Sep 17 00:00:00 2001 From: Qstick Date: Wed, 16 Mar 2022 21:14:09 -0500 Subject: [PATCH] New: Rework Movie Details view --- ...earchContent.css => InteractiveSearch.css} | 0 ...eSearchContent.js => InteractiveSearch.js} | 96 +++++++---- ...ector.js => InteractiveSearchConnector.js} | 10 +- .../InteractiveSearchRow.css | 37 ++-- .../InteractiveSearch/InteractiveSearchRow.js | 80 ++++----- .../InteractiveSearchTable.js | 16 -- .../Details/Credits/Cast/MovieCastPoster.js | 3 +- .../Details/Credits/Crew/MovieCrewPoster.js | 3 +- .../Details/Credits/MovieCreditPoster.css | 1 + .../Details/Credits/MovieCreditPosters.css | 4 + .../Details/Credits/MovieCreditPosters.js | 88 +++++----- .../Movie/Details/MovieAlternateTitles.css | 3 - .../src/Movie/Details/MovieAlternateTitles.js | 28 --- frontend/src/Movie/Details/MovieDetails.css | 7 +- frontend/src/Movie/Details/MovieDetails.js | 159 +++++++----------- .../Movie/Details/Titles/MovieTitlesRow.js | 2 +- .../Movie/Details/Titles/MovieTitlesTable.css | 9 + .../Movie/Details/Titles/MovieTitlesTable.js | 9 +- .../Details/Titles/MovieTitlesTableContent.js | 33 +--- .../MovieTitlesTableContentConnector.js | 43 ++++- .../src/Movie/History/MovieHistoryTable.css | 9 + .../src/Movie/History/MovieHistoryTable.js | 9 +- .../Search/MovieInteractiveSearchModal.js | 35 ++++ .../MovieInteractiveSearchModalConnector.js | 15 ++ .../MovieInteractiveSearchModalContent.js | 45 +++++ .../MovieFile/Editor/MovieFileEditorTable.css | 1 - .../AlternativeTitleFixture.cs | 32 ---- .../Migration/216_clean_alt_titles.cs | 17 ++ src/NzbDrone.Core/Localization/Core/en.json | 1 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 1 - .../AlternativeTitles/AlternativeTitle.cs | 33 +--- .../AlternativeTitleRepository.cs | 12 -- src/NzbDrone.Core/Movies/MovieRepository.cs | 12 ++ .../Movies/AlternativeTitleResource.cs | 12 -- .../Movies/AlternativeTitleResource.cs | 11 -- src/Radarr.Api.V4/Movies/MovieResource.cs | 2 + .../Movies/MovieTranslationResource.cs | 56 ++++++ 37 files changed, 497 insertions(+), 437 deletions(-) rename frontend/src/InteractiveSearch/{InteractiveSearchContent.css => InteractiveSearch.css} (100%) rename frontend/src/InteractiveSearch/{InteractiveSearchContent.js => InteractiveSearch.js} (69%) rename frontend/src/InteractiveSearch/{InteractiveSearchContentConnector.js => InteractiveSearchConnector.js} (90%) delete mode 100644 frontend/src/InteractiveSearch/InteractiveSearchTable.js delete mode 100644 frontend/src/Movie/Details/MovieAlternateTitles.css delete mode 100644 frontend/src/Movie/Details/MovieAlternateTitles.js create mode 100644 frontend/src/Movie/Details/Titles/MovieTitlesTable.css create mode 100644 frontend/src/Movie/History/MovieHistoryTable.css create mode 100644 frontend/src/Movie/Search/MovieInteractiveSearchModal.js create mode 100644 frontend/src/Movie/Search/MovieInteractiveSearchModalConnector.js create mode 100644 frontend/src/Movie/Search/MovieInteractiveSearchModalContent.js delete mode 100644 src/NzbDrone.Core.Test/MovieTests/AlternativeTitleServiceTests/AlternativeTitleFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/216_clean_alt_titles.cs create mode 100644 src/Radarr.Api.V4/Movies/MovieTranslationResource.cs 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 45053793c..82cbdf875 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 f4432d9e3..7b8cd2c04 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 6545102ca..1cf5f0e26 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css @@ -1,15 +1,20 @@ -.cell { +.protocol { composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 80px; } -.protocol { - composes: cell; +.title { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; - width: 80px; + 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 5b7cc6c6f..92699e8ac 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 1898e094c..028b0a807 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 7831114a2..f27131171 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 9636b2e08..c4ab53a28 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 d80f951a0..2bd05a5e0 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 7815da3ca..31d205e8b 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 ( - - + { + swiper.params.navigation.prevEl = this._swiperPrevRef; + swiper.params.navigation.nextEl = this._swiperNextRef; + swiper.navigation.init(); + swiper.navigation.update(); + }} > - {({ height, registerChild, onChildScroll, scrollTop }) => { - if (!height) { - return
; - } - - return ( -
- -
- ); - } - } - - + {items.map((credit) => ( + + + + ))} + +
); } } diff --git a/frontend/src/Movie/Details/MovieAlternateTitles.css b/frontend/src/Movie/Details/MovieAlternateTitles.css deleted file mode 100644 index 1af1ae68b..000000000 --- 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 5b0fdaeaa..000000000 --- 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 c6dbac017..38078540b 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 e453892b7..aaa44324f 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('Files')} - - - - {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 ea12bbd97..eebc9364d 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 000000000..788a53cc3 --- /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 9223a7585..1309de519 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 ae0e8011b..366bf20f7 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 000000000..788a53cc3 --- /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 f5cfd2404..e07bfa561 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 000000000..ec8987dfa --- /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 000000000..f00b1cb4d --- /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 000000000..ff85b10ed --- /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 e01af32bf..788a53cc3 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 31b9edbf0..000000000 --- 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 000000000..5f6d49836 --- /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 76fe5b98f..b3e3ebbce 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1024,6 +1024,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 4703c0110..4c776f94d 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -570,7 +570,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook 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 c3c5e2cd7..9279906e3 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 @@ namespace NzbDrone.Core.Movies.AlternativeTitles 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 413d058db..7d234b02a 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 @@ namespace NzbDrone.Core.Movies.AlternativeTitles { } - 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 ff7d38800..55ff99ce5 100644 --- a/src/NzbDrone.Core/Movies/MovieRepository.cs +++ b/src/NzbDrone.Core/Movies/MovieRepository.cs @@ -35,13 +35,16 @@ namespace NzbDrone.Core.Movies 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 @@ namespace NzbDrone.Core.Movies .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 @@ namespace NzbDrone.Core.Movies 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 1456ee888..3bcdbe523 100644 --- a/src/Radarr.Api.V3/Movies/AlternativeTitleResource.cs +++ b/src/Radarr.Api.V3/Movies/AlternativeTitleResource.cs @@ -15,13 +15,9 @@ namespace Radarr.Api.V3.Movies // 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 @@ namespace Radarr.Api.V3.Movies 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 @@ namespace Radarr.Api.V3.Movies 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 a2e25d95f..906c9a33f 100644 --- a/src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs +++ b/src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs @@ -18,9 +18,6 @@ namespace Radarr.Api.V4.Movies 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 @@ namespace Radarr.Api.V4.Movies 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 @@ namespace Radarr.Api.V4.Movies 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 29d774dbe..d6b126a4d 100644 --- a/src/Radarr.Api.V4/Movies/MovieResource.cs +++ b/src/Radarr.Api.V4/Movies/MovieResource.cs @@ -30,6 +30,7 @@ namespace Radarr.Api.V4.Movies 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 @@ namespace Radarr.Api.V4.Movies 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 000000000..e884d0f6a --- /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(); + } + } +}