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