From 12fba024f0edbb001096dc2415bfee9ec5ec5b70 Mon Sep 17 00:00:00 2001 From: devbrian <34463288+devbrian@users.noreply.github.com> Date: Sat, 6 Jul 2019 08:47:11 -0500 Subject: [PATCH] Fixed: Movie Details Tab (#3564) * History Added * History Cleanup * History Mark Failed Fix * History Lint Fix * Search Tab Initial * Interactive Search Cleanup * Files Tab + Small Backend change to MovieFile api * Reverse Movie History Items * Grabbed files are not grabbable again. * Partial movie title outline + Search not updating fix * Lint Fix + InteractiveSearch refactor * Rename movieLanguage.js to MovieLanguage.js * Fixes for qstick's comments * Rename language selector to allow for const languages * Qstick comment changes. * Activity Tabs - Language Column fixed * Movie Details - MoveStatusLabel fixed * Spaces + Lower Case added * fixed DownloadAllowed * Added padding to history and file tables * Fix class => className * Updated search to not refresh unless switching movie * lint fix * File Tab Converted to Inline Editting * FIles tab fix + Alt Titles tab implemented * lint fix * Cleanup via qstick request --- .../src/Activity/Blacklist/BlacklistRow.js | 13 + frontend/src/Activity/History/HistoryRow.js | 15 +- frontend/src/Activity/Queue/QueueRow.js | 13 + .../InteractiveImportModalContentConnector.js | 2 +- .../Interactive/InteractiveImportRow.css | 2 - .../Interactive/InteractiveImportRow.js | 11 +- ...earch.css => InteractiveSearchContent.css} | 0 ...eSearch.js => InteractiveSearchContent.js} | 17 +- ...s => InteractiveSearchContentConnector.js} | 28 +- .../InteractiveSearchFilterModalConnector.js | 7 +- .../InteractiveSearchRow.css | 11 +- .../InteractiveSearch/InteractiveSearchRow.js | 16 +- .../InteractiveSearchTable.js | 16 + frontend/src/Movie/Details/MovieDetails.js | 45 +-- .../Movie/Details/MovieDetailsConnector.js | 4 + .../src/Movie/Details/MovieStatusLabel.css | 6 +- .../src/Movie/Details/MovieStatusLabel.js | 2 +- .../src/Movie/History/MovieHistoryModal.js | 31 -- .../Movie/History/MovieHistoryModalContent.js | 136 --------- frontend/src/Movie/History/MovieHistoryRow.js | 16 +- .../src/Movie/History/MovieHistoryTable.js | 19 ++ .../History/MovieHistoryTableContent.css | 5 + .../Movie/History/MovieHistoryTableContent.js | 110 +++++++ ...s => MovieHistoryTableContentConnector.js} | 39 ++- frontend/src/Movie/MovieLanguage.js | 69 +++++ .../Search/SeasonInteractiveSearchModal.js | 36 --- .../SeasonInteractiveSearchModalConnector.js | 15 - .../SeasonInteractiveSearchModalContent.js | 48 --- frontend/src/Movie/Titles/MovieTitlesRow.js | 45 +++ frontend/src/Movie/Titles/MovieTitlesTable.js | 19 ++ .../Movie/Titles/MovieTitlesTableContent.css | 5 + .../Movie/Titles/MovieTitlesTableContent.js | 81 +++++ .../MovieTitlesTableContentConnector.js | 44 +++ .../MovieFile/Editor/MovieFileEditorModal.js | 34 --- .../Editor/MovieFileEditorModalContent.js | 284 ------------------ .../MovieFile/Editor/MovieFileEditorRow.css | 28 ++ .../MovieFile/Editor/MovieFileEditorRow.js | 217 ++++++++++--- .../MovieFile/Editor/MovieFileEditorTable.js | 21 ++ ...nt.css => MovieFileEditorTableContent.css} | 6 + .../Editor/MovieFileEditorTableContent.js | 86 ++++++ ...> MovieFileEditorTableContentConnector.js} | 49 ++- .../Editor/MovieFileRowCellPlaceholder.css | 7 + .../Editor/MovieFileRowCellPlaceholder.js | 10 + .../MovieFile/Language/SelectLanguageModal.js | 37 +++ .../Language/SelectLanguageModalContent.js | 87 ++++++ .../SelectLanguageModalContentConnector.js | 87 ++++++ frontend/src/MovieFile/MediaInfoConnector.js | 6 +- .../MovieFile/Quality/SelectQualityModal.js | 37 +++ .../Quality/SelectQualityModalContent.js | 166 ++++++++++ .../SelectQualityModalContentConnector.js | 97 ++++++ .../src/Store/Actions/blacklistActions.js | 7 + frontend/src/Store/Actions/historyActions.js | 4 +- .../src/Store/Actions/movieFileActions.js | 11 +- .../src/Store/Actions/movieHistoryActions.js | 25 +- .../src/Store/Actions/movieTitlesActions.js | 74 +++++ frontend/src/Store/Actions/queueActions.js | 6 + frontend/src/Store/Actions/releaseActions.js | 56 +--- .../DecisionEngine/DownloadDecisionMaker.cs | 2 +- .../MovieFiles/MovieFileListResource.cs | 2 + .../MovieFiles/MovieFileModule.cs | 14 +- 60 files changed, 1565 insertions(+), 821 deletions(-) rename frontend/src/InteractiveSearch/{InteractiveSearch.css => InteractiveSearchContent.css} (100%) rename frontend/src/InteractiveSearch/{InteractiveSearch.js => InteractiveSearchContent.js} (94%) rename frontend/src/InteractiveSearch/{InteractiveSearchConnector.js => InteractiveSearchContentConnector.js} (74%) create mode 100644 frontend/src/InteractiveSearch/InteractiveSearchTable.js delete mode 100644 frontend/src/Movie/History/MovieHistoryModal.js delete mode 100644 frontend/src/Movie/History/MovieHistoryModalContent.js create mode 100644 frontend/src/Movie/History/MovieHistoryTable.js create mode 100644 frontend/src/Movie/History/MovieHistoryTableContent.css create mode 100644 frontend/src/Movie/History/MovieHistoryTableContent.js rename frontend/src/Movie/History/{MovieHistoryModalContentConnector.js => MovieHistoryTableContentConnector.js} (54%) create mode 100644 frontend/src/Movie/MovieLanguage.js delete mode 100644 frontend/src/Movie/Search/SeasonInteractiveSearchModal.js delete mode 100644 frontend/src/Movie/Search/SeasonInteractiveSearchModalConnector.js delete mode 100644 frontend/src/Movie/Search/SeasonInteractiveSearchModalContent.js create mode 100644 frontend/src/Movie/Titles/MovieTitlesRow.js create mode 100644 frontend/src/Movie/Titles/MovieTitlesTable.js create mode 100644 frontend/src/Movie/Titles/MovieTitlesTableContent.css create mode 100644 frontend/src/Movie/Titles/MovieTitlesTableContent.js create mode 100644 frontend/src/Movie/Titles/MovieTitlesTableContentConnector.js delete mode 100644 frontend/src/MovieFile/Editor/MovieFileEditorModal.js delete mode 100644 frontend/src/MovieFile/Editor/MovieFileEditorModalContent.js create mode 100644 frontend/src/MovieFile/Editor/MovieFileEditorRow.css create mode 100644 frontend/src/MovieFile/Editor/MovieFileEditorTable.js rename frontend/src/MovieFile/Editor/{MovieFileEditorModalContent.css => MovieFileEditorTableContent.css} (53%) create mode 100644 frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js rename frontend/src/MovieFile/Editor/{MovieFileEditorModalContentConnector.js => MovieFileEditorTableContentConnector.js} (55%) create mode 100644 frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.css create mode 100644 frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.js create mode 100644 frontend/src/MovieFile/Language/SelectLanguageModal.js create mode 100644 frontend/src/MovieFile/Language/SelectLanguageModalContent.js create mode 100644 frontend/src/MovieFile/Language/SelectLanguageModalContentConnector.js create mode 100644 frontend/src/MovieFile/Quality/SelectQualityModal.js create mode 100644 frontend/src/MovieFile/Quality/SelectQualityModalContent.js create mode 100644 frontend/src/MovieFile/Quality/SelectQualityModalContentConnector.js create mode 100644 frontend/src/Store/Actions/movieTitlesActions.js diff --git a/frontend/src/Activity/Blacklist/BlacklistRow.js b/frontend/src/Activity/Blacklist/BlacklistRow.js index 39d4dbd0a..85e396a6e 100644 --- a/frontend/src/Activity/Blacklist/BlacklistRow.js +++ b/frontend/src/Activity/Blacklist/BlacklistRow.js @@ -6,6 +6,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo import TableRow from 'Components/Table/TableRow'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import MovieQuality from 'Movie/MovieQuality'; +import MovieLanguage from 'Movie/MovieLanguage'; import MovieTitleLink from 'Movie/MovieTitleLink'; import BlacklistDetailsModal from './BlacklistDetailsModal'; import styles from './BlacklistRow.css'; @@ -42,6 +43,7 @@ class BlacklistRow extends Component { movie, sourceTitle, quality, + languages, date, protocol, indexer, @@ -82,6 +84,16 @@ class BlacklistRow extends Component { ); } + if (name === 'language') { + return ( + + + + ); + } + if (name === 'quality') { return ( + + + ); + } + if (name === 'quality') { return ( @@ -193,8 +205,7 @@ class HistoryRow extends Component { HistoryRow.propTypes = { movieId: PropTypes.number, movie: PropTypes.object.isRequired, - language: PropTypes.object.isRequired, - languageCutoffNotMet: PropTypes.bool.isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, quality: PropTypes.object.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired, eventType: PropTypes.string.isRequired, diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js index 048492f2d..3e142726c 100644 --- a/frontend/src/Activity/Queue/QueueRow.js +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -10,6 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import MovieQuality from 'Movie/MovieQuality'; +import MovieLanguage from 'Movie/MovieLanguage'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import MovieTitleLink from 'Movie/MovieTitleLink'; import QueueStatusCell from './QueueStatusCell'; @@ -69,6 +70,7 @@ class QueueRow extends Component { errorMessage, movie, quality, + languages, protocol, indexer, outputPath, @@ -145,6 +147,16 @@ class QueueRow extends Component { ); } + if (name === 'language') { + return ( + + + + ); + } + if (name === 'quality') { return ( @@ -297,6 +309,7 @@ QueueRow.propTypes = { errorMessage: PropTypes.string, movie: PropTypes.object.isRequired, quality: PropTypes.object.isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, protocol: PropTypes.string.isRequired, indexer: PropTypes.string, outputPath: PropTypes.string, diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index a28919e29..c6db12d01 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -128,7 +128,7 @@ class InteractiveImportModalContentConnector extends Component { folderName: item.folderName, movieId: movie.id, quality, - language, + languages: [language], downloadId: this.props.downloadId }); } diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css index b304538b0..4cafa144a 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css @@ -7,8 +7,6 @@ .quality, .language { composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - text-align: center; } .label { diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index 090ef1fd9..4e32aa912 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -9,7 +9,7 @@ import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import Popover from 'Components/Tooltip/Popover'; import MovieQuality from 'Movie/MovieQuality'; -// import MovieLanguage from 'Movie/MovieLanguage'; +import MovieLanguage from 'Movie/MovieLanguage'; import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; @@ -152,7 +152,8 @@ class InteractiveImportRow extends Component { const showMoviePlaceholder = isSelected && !movie; const showQualityPlaceholder = isSelected && !quality; const showLanguagePlaceholder = isSelected && !language; - + // TODO - Placeholder till we implement selection of multiple languages + const languages = [language]; return ( } - {/* { + { !showLanguagePlaceholder && !!language && - } */} + } diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.css b/frontend/src/InteractiveSearch/InteractiveSearchContent.css similarity index 100% rename from frontend/src/InteractiveSearch/InteractiveSearch.css rename to frontend/src/InteractiveSearch/InteractiveSearchContent.css diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.js b/frontend/src/InteractiveSearch/InteractiveSearchContent.js similarity index 94% rename from frontend/src/InteractiveSearch/InteractiveSearch.js rename to frontend/src/InteractiveSearch/InteractiveSearchContent.js index 453ebd468..949619ce9 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchContent.js @@ -9,7 +9,7 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; import InteractiveSearchRow from './InteractiveSearchRow'; -import styles from './InteractiveSearch.css'; +import styles from './InteractiveSearchContent.css'; const columns = [ { @@ -48,6 +48,12 @@ const columns = [ isSortable: true, isVisible: true }, + { + name: 'languageWeight', + label: 'Language', + isSortable: true, + isVisible: true + }, { name: 'qualityWeight', label: 'Quality', @@ -70,7 +76,7 @@ const columns = [ } ]; -function InteractiveSearch(props) { +function InteractiveSearchContent(props) { const { searchPayload, isFetching, @@ -83,7 +89,6 @@ function InteractiveSearch(props) { customFilters, sortKey, sortDirection, - type, longDateFormat, timeFormat, onSortPress, @@ -101,7 +106,6 @@ function InteractiveSearch(props) { customFilters={customFilters} buttonComponent={PageMenuButton} filterModalConnectorComponent={InteractiveSearchFilterModalConnector} - filterModalConnectorComponentProps={{ type }} onFilterSelect={onFilterSelect} /> @@ -169,7 +173,7 @@ function InteractiveSearch(props) { ); } -InteractiveSearch.propTypes = { +InteractiveSearchContent.propTypes = { searchPayload: PropTypes.object.isRequired, isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, @@ -181,7 +185,6 @@ InteractiveSearch.propTypes = { customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, sortDirection: PropTypes.string, - type: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired, onSortPress: PropTypes.func.isRequired, @@ -189,4 +192,4 @@ InteractiveSearch.propTypes = { onGrabPress: PropTypes.func.isRequired }; -export default InteractiveSearch; +export default InteractiveSearchContent; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchConnector.js b/frontend/src/InteractiveSearch/InteractiveSearchContentConnector.js similarity index 74% rename from frontend/src/InteractiveSearch/InteractiveSearchConnector.js rename to frontend/src/InteractiveSearch/InteractiveSearchContentConnector.js index c9f90472b..f4432d9e3 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchConnector.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchContentConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import * as releaseActions from 'Store/Actions/releaseActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; -import InteractiveSearch from './InteractiveSearch'; +import InteractiveSearchContent from './InteractiveSearchContent'; -function createMapStateToProps(appState, { type }) { +function createMapStateToProps(appState) { return createSelector( (state) => state.releases.items.length, - createClientSideCollectionSelector('releases', `releases.${type}`), + createClientSideCollectionSelector('releases'), createUISettingsSelector(), (totalReleasesCount, releases, uiSettings) => { return { @@ -29,15 +29,16 @@ function createMapDispatchToProps(dispatch, props) { dispatch(releaseActions.fetchReleases(payload)); }, + dispatchClearReleases(payload) { + dispatch(releaseActions.clearReleases(payload)); + }, + onSortPress(sortKey, sortDirection) { dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection })); }, onFilterSelect(selectedFilterKey) { - const action = props.type === 'episode' ? - releaseActions.setEpisodeReleasesFilter : - releaseActions.setSeasonReleasesFilter; - + const action = releaseActions.setReleasesFilter; dispatch(action({ selectedFilterKey })); }, @@ -47,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) { }; } -class InteractiveSearchConnector extends Component { +class InteractiveSearchContentConnector extends Component { // // Lifecycle @@ -61,7 +62,6 @@ class InteractiveSearchConnector extends Component { // If search results are not yet isPopulated fetch them, // otherwise re-show the existing props. - if (!isPopulated) { dispatchFetchReleases(searchPayload); } @@ -73,22 +73,24 @@ class InteractiveSearchConnector extends Component { render() { const { dispatchFetchReleases, + dispatchClearReleases, ...otherProps } = this.props; return ( - ); } } -InteractiveSearchConnector.propTypes = { +InteractiveSearchContentConnector.propTypes = { searchPayload: PropTypes.object.isRequired, isPopulated: PropTypes.bool.isRequired, - dispatchFetchReleases: PropTypes.func.isRequired + dispatchFetchReleases: PropTypes.func.isRequired, + dispatchClearReleases: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector); +export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector); diff --git a/frontend/src/InteractiveSearch/InteractiveSearchFilterModalConnector.js b/frontend/src/InteractiveSearch/InteractiveSearchFilterModalConnector.js index dcbcf340f..f52db8bbd 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchFilterModalConnector.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchFilterModalConnector.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { setEpisodeReleasesFilter, setSeasonReleasesFilter } from 'Store/Actions/releaseActions'; +import { setReleasesFilter } from 'Store/Actions/releaseActions'; import FilterModal from 'Components/Filter/FilterModal'; function createMapStateToProps() { @@ -20,10 +20,7 @@ function createMapStateToProps() { function createMapDispatchToProps(dispatch, props) { return { dispatchSetFilter(payload) { - const action = props.type === 'episode' ? - setEpisodeReleasesFilter: - setSeasonReleasesFilter; - + const action = setReleasesFilter; dispatch(action(payload)); } }; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css b/frontend/src/InteractiveSearch/InteractiveSearchRow.css index 80a8b8bbe..58a2446ee 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css @@ -1,13 +1,10 @@ -.title { +.quality, +.language { composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - word-break: break-all; } -.quality { - composes: cell from '~Components/Table/Cells/TableRowCell.css'; - - text-align: center; +.language { + width: 100px; } .rejected, diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.js b/frontend/src/InteractiveSearch/InteractiveSearchRow.js index f303394c7..1fa78899c 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.js @@ -11,10 +11,11 @@ import ConfirmModal from 'Components/Modal/ConfirmModal'; import TableRow from 'Components/Table/TableRow'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import Popover from 'Components/Tooltip/Popover'; -import EpisodeQuality from 'Episode/EpisodeQuality'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import Peers from './Peers'; import styles from './InteractiveSearchRow.css'; +import MovieQuality from 'Movie/MovieQuality'; +import MovieLanguage from 'Movie/MovieLanguage'; function getDownloadIcon(isGrabbing, isGrabbed, grabError) { if (isGrabbing) { @@ -111,6 +112,7 @@ class InteractiveSearchRow extends Component { seeders, leechers, quality, + languages, rejections, downloadAllowed, isGrabbing, @@ -159,8 +161,14 @@ class InteractiveSearchRow extends Component { } + + + + - @@ -199,6 +207,7 @@ class InteractiveSearchRow extends Component { name={getDownloadIcon(isGrabbing, isGrabbed, grabError)} kind={grabError ? kinds.DANGER : kinds.DEFAULT} title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)} + isDisabled={isGrabbed} isSpinning={isGrabbing} onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress} /> @@ -208,7 +217,7 @@ class InteractiveSearchRow extends Component { isOpen={this.state.isConfirmGrabModalOpen} kind={kinds.WARNING} title="Grab Release" - message={`Sonarr was unable to determine which series and episode this release was for. Sonarr may be unable to automatically import this release. Do you want to grab '${title}'?`} + message={`Radarr was unable to determine which movie this release was for. Radarr may be unable to automatically import this release. Do you want to grab '${title}'?`} confirmLabel="Grab" onConfirm={this.onGrabConfirm} onCancel={this.onGrabCancel} @@ -233,6 +242,7 @@ InteractiveSearchRow.propTypes = { seeders: PropTypes.number, leechers: PropTypes.number, quality: PropTypes.object.isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, rejections: PropTypes.arrayOf(PropTypes.string).isRequired, downloadAllowed: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired, diff --git a/frontend/src/InteractiveSearch/InteractiveSearchTable.js b/frontend/src/InteractiveSearch/InteractiveSearchTable.js new file mode 100644 index 000000000..af6585435 --- /dev/null +++ b/frontend/src/InteractiveSearch/InteractiveSearchTable.js @@ -0,0 +1,16 @@ +import React from 'react'; +import InteractiveSearchContentConnector from './InteractiveSearchContentConnector'; + +function InteractiveSearchTable(props) { + + return ( + + ); +} + +InteractiveSearchTable.propTypes = { +}; + +export default InteractiveSearchTable; diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index 343350d96..3c47120d9 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -22,15 +22,17 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import Popover from 'Components/Tooltip/Popover'; -import MovieFileEditorModal from 'MovieFile/Editor/MovieFileEditorModal'; +import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import MoviePoster from 'Movie/MoviePoster'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; -import MovieHistoryModal from 'Movie/History/MovieHistoryModal'; +import MovieHistoryTable from 'Movie/History/MovieHistoryTable'; +import MovieTitlesTable from 'Movie/Titles/MovieTitlesTable'; import MovieAlternateTitles from './MovieAlternateTitles'; import MovieDetailsLinks from './MovieDetailsLinks'; +import InteractiveSearchTable from '../../InteractiveSearch/InteractiveSearchTable'; // import MovieTagsConnector from './MovieTagsConnector'; import styles from './MovieDetails.css'; import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal'; @@ -68,7 +70,6 @@ class MovieDetails extends Component { isManageEpisodesOpen: false, isEditMovieModalOpen: false, isDeleteMovieModalOpen: false, - isMovieHistoryModalOpen: false, isInteractiveImportModalOpen: false, allExpanded: false, allCollapsed: false, @@ -123,14 +124,6 @@ class MovieDetails extends Component { this.setState({ isDeleteMovieModalOpen: false }); } - onMovieHistoryPress = () => { - this.setState({ isMovieHistoryModalOpen: true }); - } - - onMovieHistoryModalClose = () => { - this.setState({ isMovieHistoryModalOpen: false }); - } - onExpandAllPress = () => { const { allExpanded, @@ -195,10 +188,8 @@ class MovieDetails extends Component { const { isOrganizeModalOpen, - isManageEpisodesOpen, isEditMovieModalOpen, isDeleteMovieModalOpen, - isMovieHistoryModalOpen, isInteractiveImportModalOpen, overviewHeight } = this.state; @@ -488,19 +479,27 @@ class MovieDetails extends Component { -

Any content 1

+
-

Any content 2

+
-

Any content 3

+
-

Any content 4

+
@@ -512,18 +511,6 @@ class MovieDetails extends Component { onModalClose={this.onOrganizeModalClose} /> - - - - { this.props.clearMovieFiles(); this.props.clearQueueDetails(); + this.props.clearReleases(); } // @@ -220,6 +223,7 @@ MovieDetailsConnector.propTypes = { isRenamingMovie: PropTypes.bool.isRequired, fetchMovieFiles: PropTypes.func.isRequired, clearMovieFiles: PropTypes.func.isRequired, + clearReleases: PropTypes.func.isRequired, toggleMovieMonitored: PropTypes.func.isRequired, fetchQueueDetails: PropTypes.func.isRequired, clearQueueDetails: PropTypes.func.isRequired, diff --git a/frontend/src/Movie/Details/MovieStatusLabel.css b/frontend/src/Movie/Details/MovieStatusLabel.css index 5300ab69e..0be337a50 100644 --- a/frontend/src/Movie/Details/MovieStatusLabel.css +++ b/frontend/src/Movie/Details/MovieStatusLabel.css @@ -5,12 +5,12 @@ .downloaded { padding-left: 2px; - border-left: 4px solid $dangerColor; + border-left: 4px solid $successColor; } -.unaired { +.unreleased { padding-left: 2px; - border-left: 4px solid $gray; + border-left: 4px solid $primaryColor; } .unmonitored { diff --git a/frontend/src/Movie/Details/MovieStatusLabel.js b/frontend/src/Movie/Details/MovieStatusLabel.js index 684a1ec86..8c233f66c 100644 --- a/frontend/src/Movie/Details/MovieStatusLabel.js +++ b/frontend/src/Movie/Details/MovieStatusLabel.js @@ -18,7 +18,7 @@ function getMovieStatus(hasFile, isMonitored, inCinemas) { return 'Missing'; } - return 'Unaired'; + return 'Unreleased'; } function MovieStatusLabel(props) { diff --git a/frontend/src/Movie/History/MovieHistoryModal.js b/frontend/src/Movie/History/MovieHistoryModal.js deleted file mode 100644 index 8f6d8c034..000000000 --- a/frontend/src/Movie/History/MovieHistoryModal.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import MovieHistoryModalContentConnector from './MovieHistoryModalContentConnector'; - -function MovieHistoryModal(props) { - const { - isOpen, - onModalClose, - ...otherProps - } = props; - - return ( - - - - ); -} - -MovieHistoryModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default MovieHistoryModal; diff --git a/frontend/src/Movie/History/MovieHistoryModalContent.js b/frontend/src/Movie/History/MovieHistoryModalContent.js deleted file mode 100644 index 2635c6c12..000000000 --- a/frontend/src/Movie/History/MovieHistoryModalContent.js +++ /dev/null @@ -1,136 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import MovieHistoryRowConnector from './MovieHistoryRowConnector'; -const columns = [ - { - name: 'eventType', - isVisible: true - }, - { - name: 'episode', - label: 'Episode', - isVisible: true - }, - { - name: 'sourceTitle', - label: 'Source Title', - isVisible: true - }, - { - name: 'language', - label: 'Language', - isVisible: true - }, - { - name: 'quality', - label: 'Quality', - isVisible: true - }, - { - name: 'date', - label: 'Date', - isVisible: true - }, - { - name: 'details', - label: 'Details', - isVisible: true - }, - { - name: 'actions', - label: 'Actions', - isVisible: true - } -]; - -class MovieHistoryModalContent extends Component { - - // - // Render - - render() { - const { - seasonNumber, - isFetching, - isPopulated, - error, - items, - onMarkAsFailedPress, - onModalClose - } = this.props; - - const fullSeries = seasonNumber == null; - const hasItems = !!items.length; - - return ( - - - History - - - - { - isFetching && - - } - - { - !isFetching && !!error && -
Unable to load history.
- } - - { - isPopulated && !hasItems && !error && -
No history.
- } - - { - isPopulated && hasItems && !error && - - - { - items.map((item) => { - return ( - - ); - }) - } - -
- } -
- - - - -
- ); - } -} - -MovieHistoryModalContent.propTypes = { - seasonNumber: PropTypes.number, - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - onMarkAsFailedPress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default MovieHistoryModalContent; diff --git a/frontend/src/Movie/History/MovieHistoryRow.js b/frontend/src/Movie/History/MovieHistoryRow.js index 259052b1a..e21cc82dd 100644 --- a/frontend/src/Movie/History/MovieHistoryRow.js +++ b/frontend/src/Movie/History/MovieHistoryRow.js @@ -9,6 +9,7 @@ import TableRow from 'Components/Table/TableRow'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import Popover from 'Components/Tooltip/Popover'; import MovieQuality from 'Movie/MovieQuality'; +import MovieLanguage from 'Movie/MovieLanguage'; import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; import styles from './MovieHistoryRow.css'; @@ -20,7 +21,8 @@ function getTitle(eventType) { case 'downloadFolderImported': return 'Download Folder Imported'; case 'downloadFailed': return 'Download Failed'; case 'episodeFileDeleted': return 'Episode File Deleted'; - case 'episodeFileRenamed': return 'Episode File Renamed'; + case 'movieFileDeleted': return 'Movie File Deleted'; + case 'movieFolderImported': return 'Movie Folder Imported'; default: return 'Unknown'; } } @@ -62,10 +64,10 @@ class MovieHistoryRow extends Component { eventType, sourceTitle, quality, + languages, qualityCutoffNotMet, date, data - // movie, } = this.props; const { @@ -83,6 +85,12 @@ class MovieHistoryRow extends Component { {sourceTitle}
+ + + + + ); +} + +MovieHistoryTable.propTypes = { +}; + +export default MovieHistoryTable; diff --git a/frontend/src/Movie/History/MovieHistoryTableContent.css b/frontend/src/Movie/History/MovieHistoryTableContent.css new file mode 100644 index 000000000..ed117bd32 --- /dev/null +++ b/frontend/src/Movie/History/MovieHistoryTableContent.css @@ -0,0 +1,5 @@ +.blankpad { + padding-left:2em; + padding-top: 10px; + padding-bottom: 10px; +} diff --git a/frontend/src/Movie/History/MovieHistoryTableContent.js b/frontend/src/Movie/History/MovieHistoryTableContent.js new file mode 100644 index 000000000..0f2e44769 --- /dev/null +++ b/frontend/src/Movie/History/MovieHistoryTableContent.js @@ -0,0 +1,110 @@ +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 MovieHistoryRowConnector from './MovieHistoryRowConnector'; +import styles from './MovieHistoryTableContent.css'; + +const columns = [ + { + name: 'eventType', + isVisible: true + }, + { + name: 'sourceTitle', + label: 'Source Title', + isVisible: true + }, + { + name: 'languages', + label: 'Languages', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'date', + label: 'Date', + isVisible: true + }, + { + name: 'details', + label: 'Details', + isVisible: true + }, + { + name: 'actions', + label: 'Actions', + isVisible: true + } +]; + +class MovieHistoryTableContent extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + onMarkAsFailedPress + } = this.props; + + const hasItems = !!items.length; + + return ( +
+ { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to load history
+ } + + { + isPopulated && !hasItems && !error && +
No history
+ } + + { + isPopulated && hasItems && !error && + + + { + items.reverse().map((item) => { + return ( + + ); + }) + } + +
+ } +
+ ); + } +} + +MovieHistoryTableContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onMarkAsFailedPress: PropTypes.func.isRequired +}; + +export default MovieHistoryTableContent; diff --git a/frontend/src/Movie/History/MovieHistoryModalContentConnector.js b/frontend/src/Movie/History/MovieHistoryTableContentConnector.js similarity index 54% rename from frontend/src/Movie/History/MovieHistoryModalContentConnector.js rename to frontend/src/Movie/History/MovieHistoryTableContentConnector.js index 581069cd5..709e9ce2c 100644 --- a/frontend/src/Movie/History/MovieHistoryModalContentConnector.js +++ b/frontend/src/Movie/History/MovieHistoryTableContentConnector.js @@ -2,14 +2,14 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { fetchMovieHistory, clearMovieHistory, seriesHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions'; -import MovieHistoryModalContent from './MovieHistoryModalContent'; +import { fetchMovieHistory, clearMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions'; +import MovieHistoryTableContent from './MovieHistoryTableContent'; function createMapStateToProps() { return createSelector( - (state) => state.moviesHistory, - (seriesHistory) => { - return seriesHistory; + (state) => state.movieHistory, + (movieHistory) => { + return movieHistory; } ); } @@ -17,23 +17,21 @@ function createMapStateToProps() { const mapDispatchToProps = { fetchMovieHistory, clearMovieHistory, - seriesHistoryMarkAsFailed + movieHistoryMarkAsFailed }; -class MovieHistoryModalContentConnector extends Component { +class MovieHistoryTableContentConnector extends Component { // // Lifecycle componentDidMount() { const { - seriesId, - seasonNumber + movieId } = this.props; this.props.fetchMovieHistory({ - seriesId, - seasonNumber + movieId }); } @@ -46,14 +44,12 @@ class MovieHistoryModalContentConnector extends Component { onMarkAsFailedPress = (historyId) => { const { - seriesId, - seasonNumber + movieId } = this.props; - this.props.seriesHistoryMarkAsFailed({ + this.props.movieHistoryMarkAsFailed({ historyId, - seriesId, - seasonNumber + movieId }); } @@ -62,7 +58,7 @@ class MovieHistoryModalContentConnector extends Component { render() { return ( - @@ -70,12 +66,11 @@ class MovieHistoryModalContentConnector extends Component { } } -MovieHistoryModalContentConnector.propTypes = { - seriesId: PropTypes.number.isRequired, - seasonNumber: PropTypes.number, +MovieHistoryTableContentConnector.propTypes = { + movieId: PropTypes.number.isRequired, fetchMovieHistory: PropTypes.func.isRequired, clearMovieHistory: PropTypes.func.isRequired, - seriesHistoryMarkAsFailed: PropTypes.func.isRequired + movieHistoryMarkAsFailed: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryTableContentConnector); diff --git a/frontend/src/Movie/MovieLanguage.js b/frontend/src/Movie/MovieLanguage.js new file mode 100644 index 000000000..0527c38bd --- /dev/null +++ b/frontend/src/Movie/MovieLanguage.js @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Label from 'Components/Label'; +import { kinds, tooltipPositions } from 'Helpers/Props'; +import Popover from 'Components/Tooltip/Popover'; + +function MovieLanguage(props) { + const { + className, + languages, + isCutoffNotMet + } = props; + + if (!languages) { + return null; + } + + if (languages.length === 1) { + return ( + + ); + } + + return ( + + Multi-Language + + } + title="Languages" + body={ +
    + { + languages.map((language) => { + return ( +
  • + {language.name} +
  • + ); + }) + } +
+ } + position={tooltipPositions.LEFT} + /> + ); +} + +MovieLanguage.propTypes = { + className: PropTypes.string, + languages: PropTypes.arrayOf(PropTypes.object), + isCutoffNotMet: PropTypes.bool +}; + +MovieLanguage.defaultProps = { + isCutoffNotMet: true +}; + +export default MovieLanguage; diff --git a/frontend/src/Movie/Search/SeasonInteractiveSearchModal.js b/frontend/src/Movie/Search/SeasonInteractiveSearchModal.js deleted file mode 100644 index 7973affba..000000000 --- a/frontend/src/Movie/Search/SeasonInteractiveSearchModal.js +++ /dev/null @@ -1,36 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import SeasonInteractiveSearchModalContent from './SeasonInteractiveSearchModalContent'; - -function SeasonInteractiveSearchModal(props) { - const { - isOpen, - seriesId, - seasonNumber, - onModalClose - } = props; - - return ( - - - - ); -} - -SeasonInteractiveSearchModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - seriesId: PropTypes.number.isRequired, - seasonNumber: PropTypes.number.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SeasonInteractiveSearchModal; diff --git a/frontend/src/Movie/Search/SeasonInteractiveSearchModalConnector.js b/frontend/src/Movie/Search/SeasonInteractiveSearchModalConnector.js deleted file mode 100644 index e270ebdec..000000000 --- a/frontend/src/Movie/Search/SeasonInteractiveSearchModalConnector.js +++ /dev/null @@ -1,15 +0,0 @@ -import { connect } from 'react-redux'; -import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; -import SeasonInteractiveSearchModal from './SeasonInteractiveSearchModal'; - -function createMapDispatchToProps(dispatch, props) { - return { - onModalClose() { - dispatch(cancelFetchReleases()); - dispatch(clearReleases()); - props.onModalClose(); - } - }; -} - -export default connect(null, createMapDispatchToProps)(SeasonInteractiveSearchModal); diff --git a/frontend/src/Movie/Search/SeasonInteractiveSearchModalContent.js b/frontend/src/Movie/Search/SeasonInteractiveSearchModalContent.js deleted file mode 100644 index 51ca9702c..000000000 --- a/frontend/src/Movie/Search/SeasonInteractiveSearchModalContent.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Button from 'Components/Link/Button'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector'; - -function SeasonInteractiveSearchModalContent(props) { - const { - seriesId, - seasonNumber, - onModalClose - } = props; - - return ( - - - Interactive Search - - - - - - - - - - - ); -} - -SeasonInteractiveSearchModalContent.propTypes = { - seriesId: PropTypes.number.isRequired, - seasonNumber: PropTypes.number.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default SeasonInteractiveSearchModalContent; diff --git a/frontend/src/Movie/Titles/MovieTitlesRow.js b/frontend/src/Movie/Titles/MovieTitlesRow.js new file mode 100644 index 000000000..d458170d1 --- /dev/null +++ b/frontend/src/Movie/Titles/MovieTitlesRow.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import MovieLanguage from 'Movie/MovieLanguage'; + +class MovieTitlesRow extends Component { + + // + // Render + + render() { + const { + title, + language + } = this.props; + + // TODO - Fix languages to all take arrays + const languages = [language]; + + return ( + + + + {title} + + + + + + + + ); + } +} + +MovieTitlesRow.propTypes = { + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + language: PropTypes.object.isRequired +}; + +export default MovieTitlesRow; diff --git a/frontend/src/Movie/Titles/MovieTitlesTable.js b/frontend/src/Movie/Titles/MovieTitlesTable.js new file mode 100644 index 000000000..9223a7585 --- /dev/null +++ b/frontend/src/Movie/Titles/MovieTitlesTable.js @@ -0,0 +1,19 @@ +import React from 'react'; +import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector'; + +function MovieTitlesTable(props) { + const { + ...otherProps + } = props; + + return ( + + ); +} + +MovieTitlesTable.propTypes = { +}; + +export default MovieTitlesTable; diff --git a/frontend/src/Movie/Titles/MovieTitlesTableContent.css b/frontend/src/Movie/Titles/MovieTitlesTableContent.css new file mode 100644 index 000000000..ed117bd32 --- /dev/null +++ b/frontend/src/Movie/Titles/MovieTitlesTableContent.css @@ -0,0 +1,5 @@ +.blankpad { + padding-left:2em; + padding-top: 10px; + padding-bottom: 10px; +} diff --git a/frontend/src/Movie/Titles/MovieTitlesTableContent.js b/frontend/src/Movie/Titles/MovieTitlesTableContent.js new file mode 100644 index 000000000..6854c1f05 --- /dev/null +++ b/frontend/src/Movie/Titles/MovieTitlesTableContent.js @@ -0,0 +1,81 @@ +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 styles from './MovieTitlesTableContent.css'; +import MovieTitlesRow from './MovieTitlesRow'; +const columns = [ + { + name: 'altTitle', + label: 'Alternative Title', + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: true + } +]; + +class MovieTitlesTableContent extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items + } = this.props; + + const hasItems = !!items.length; + return ( +
+ { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to load alternative titles.
+ } + + { + isPopulated && !hasItems && !error && +
No alternative titles.
+ } + + { + isPopulated && hasItems && !error && + + + { + items.reverse().map((item) => { + return ( + + ); + }) + } + +
+ } +
+ ); + } +} + +MovieTitlesTableContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default MovieTitlesTableContent; diff --git a/frontend/src/Movie/Titles/MovieTitlesTableContentConnector.js b/frontend/src/Movie/Titles/MovieTitlesTableContentConnector.js new file mode 100644 index 000000000..136a7e994 --- /dev/null +++ b/frontend/src/Movie/Titles/MovieTitlesTableContentConnector.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import MovieTitlesTableContent from './MovieTitlesTableContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.movies, + (movies) => { + return movies; + } + ); +} + +const mapDispatchToProps = { +// fetchMovies +}; + +class MovieTitlesTableContentConnector extends Component { + + // + // Render + + render() { + const movie = this.props.items.filter((obj) => { + return obj.id === this.props.movieId; + }); + + return ( + + ); + } +} + +MovieTitlesTableContentConnector.propTypes = { + movieId: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector); diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorModal.js b/frontend/src/MovieFile/Editor/MovieFileEditorModal.js deleted file mode 100644 index eae5a7b6b..000000000 --- a/frontend/src/MovieFile/Editor/MovieFileEditorModal.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import Modal from 'Components/Modal/Modal'; -import MovieFileEditorModalContentConnector from './MovieFileEditorModalContentConnector'; - -function MovieFileEditorModal(props) { - const { - isOpen, - onModalClose, - ...otherProps - } = props; - - return ( - - { - isOpen && - - } - - ); -} - -MovieFileEditorModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default MovieFileEditorModal; diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorModalContent.js b/frontend/src/MovieFile/Editor/MovieFileEditorModalContent.js deleted file mode 100644 index 94a0e7ac1..000000000 --- a/frontend/src/MovieFile/Editor/MovieFileEditorModalContent.js +++ /dev/null @@ -1,284 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; -import selectAll from 'Utilities/Table/selectAll'; -import toggleSelected from 'Utilities/Table/toggleSelected'; -import { kinds } from 'Helpers/Props'; -import ConfirmModal from 'Components/Modal/ConfirmModal'; -import Button from 'Components/Link/Button'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -import SelectInput from 'Components/Form/SelectInput'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import Table from 'Components/Table/Table'; -import TableBody from 'Components/Table/TableBody'; -import MovieFileEditorRow from './MovieFileEditorRow'; -import styles from './MovieFileEditorModalContent.css'; - -const columns = [ - { - name: 'episodeNumber', - label: 'Episode', - isVisible: true - }, - { - name: 'relativePath', - label: 'Relative Path', - isVisible: true - }, - { - name: 'airDateUtc', - label: 'Air Date', - isVisible: true - }, - { - name: 'language', - label: 'Language', - isVisible: true - }, - { - name: 'quality', - label: 'Quality', - isVisible: true - } -]; - -class MovieFileEditorModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - allSelected: false, - allUnselected: false, - lastToggled: null, - selectedState: {}, - isConfirmDeleteModalOpen: false - }; - } - - componentDidUpdate(prevProps) { - if (hasDifferentItems(prevProps.items, this.props.items)) { - this.setState((state) => { - return removeOldSelectedState(state, prevProps.items); - }); - } - } - - // - // Control - - getSelectedIds = () => { - const selectedIds = getSelectedIds(this.state.selectedState); - - return selectedIds.reduce((acc, id) => { - const matchingItem = this.props.items.find((item) => item.id === id); - - if (matchingItem && !acc.includes(matchingItem.episodeFileId)) { - acc.push(matchingItem.episodeFileId); - } - - return acc; - }, []); - } - - // - // Listeners - - onSelectAllChange = ({ value }) => { - this.setState(selectAll(this.state.selectedState, value)); - } - - onSelectedChange = ({ id, value, shiftKey = false }) => { - this.setState((state) => { - return toggleSelected(state, this.props.items, id, value, shiftKey); - }); - } - - onDeletePress = () => { - this.setState({ isConfirmDeleteModalOpen: true }); - } - - onConfirmDelete = () => { - this.setState({ isConfirmDeleteModalOpen: false }); - this.props.onDeletePress(this.getSelectedIds()); - } - - onConfirmDeleteModalClose = () => { - this.setState({ isConfirmDeleteModalOpen: false }); - } - - onLanguageChange = ({ value }) => { - const selectedIds = this.getSelectedIds(); - - if (!selectedIds.length) { - return; - } - - this.props.onLanguageChange(selectedIds, parseInt(value)); - } - - onQualityChange = ({ value }) => { - const selectedIds = this.getSelectedIds(); - - if (!selectedIds.length) { - return; - } - - this.props.onQualityChange(selectedIds, parseInt(value)); - } - - // - // Render - - render() { - const { - isDeleting, - items, - languages, - qualities, - onModalClose - } = this.props; - - const { - allSelected, - allUnselected, - selectedState, - isConfirmDeleteModalOpen - } = this.state; - - const languageOptions = _.reduceRight(languages, (acc, language) => { - acc.push({ - key: language.id, - value: language.name - }); - - return acc; - }, [{ key: 'selectLanguage', value: 'Select Language', disabled: true }]); - - const qualityOptions = _.reduceRight(qualities, (acc, quality) => { - acc.push({ - key: quality.id, - value: quality.name - }); - - return acc; - }, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]); - - const hasSelectedFiles = this.getSelectedIds().length > 0; - - return ( - - - Manage Episodes - - - - { - !items.length && -
- No episode files to manage. -
- } - - { - !!items.length && - - - { - items.map((item) => { - return ( - - ); - }) - } - -
- } -
- - -
- - Delete - - -
- -
- -
- -
-
- - -
- - -
- ); - } -} - -MovieFileEditorModalContent.propTypes = { - seasonNumber: PropTypes.number, - isDeleting: PropTypes.bool.isRequired, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - languages: PropTypes.arrayOf(PropTypes.object).isRequired, - qualities: PropTypes.arrayOf(PropTypes.object).isRequired, - onDeletePress: PropTypes.func.isRequired, - onLanguageChange: PropTypes.func.isRequired, - onQualityChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default MovieFileEditorModalContent; diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorRow.css b/frontend/src/MovieFile/Editor/MovieFileEditorRow.css new file mode 100644 index 000000000..65746a6f0 --- /dev/null +++ b/frontend/src/MovieFile/Editor/MovieFileEditorRow.css @@ -0,0 +1,28 @@ +.title { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + word-break: break-all; + } + + .quality, + .language { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + } + + .language { + width: 100px; + } + + .rejected, + .download { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 50px; + } + + .age, + .size { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + white-space: nowrap; + } diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorRow.js b/frontend/src/MovieFile/Editor/MovieFileEditorRow.js index bab071699..b509a1914 100644 --- a/frontend/src/MovieFile/Editor/MovieFileEditorRow.js +++ b/frontend/src/MovieFile/Editor/MovieFileEditorRow.js @@ -1,62 +1,195 @@ import PropTypes from 'prop-types'; -import React from 'react'; -import Label from 'Components/Label'; -import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import React, { Component } from 'react'; +import IconButton from 'Components/Link/IconButton'; +import { icons, kinds } from 'Helpers/Props'; import TableRow from 'Components/Table/TableRow'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton'; import MovieQuality from 'Movie/MovieQuality'; +import MovieLanguage from 'Movie/MovieLanguage'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import SelectQualityModal from 'MovieFile/Quality/SelectQualityModal'; +import SelectLanguageModal from 'MovieFile/Language/SelectLanguageModal'; +import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes'; +import MediaInfoConnector from 'MovieFile/MediaInfoConnector'; +import MovieFileRowCellPlaceholder from './MovieFileRowCellPlaceholder'; +import styles from './MovieFileEditorRow.css'; -function MovieFileEditorRow(props) { - const { - id, - relativePath, - airDateUtc, - language, - quality, - isSelected, - onSelectedChange - } = props; +class MovieFileEditorRow extends Component { - return ( - - + // + // Lifecycle - - {relativePath} - + constructor(props, context) { + super(props, context); - + this.state = { + isSelectQualityModalOpen: false, + isSelectLanguageModalOpen: false, + isConfirmDeleteModalOpen: false + }; + } - - - + // + // Listeners - - { + this.setState({ isSelectQualityModalOpen: true }); + } + + onSelectLanguagePress = () => { + this.setState({ isSelectLanguageModalOpen: true }); + } + + onSelectQualityModalClose = () => { + this.setState({ isSelectQualityModalOpen: false }); + } + + onSelectLanguageModalClose = () => { + this.setState({ isSelectLanguageModalOpen: false }); + } + + onDeletePress = () => { + this.setState({ isConfirmDeleteModalOpen: true }); + } + + onConfirmDelete = () => { + this.setState({ isConfirmDeleteModalOpen: false }); + + this.props.onDeletePress(this.props.id); + } + + onConfirmDeleteModalClose = () => { + this.setState({ isConfirmDeleteModalOpen: false }); + } + + // + // Render + + render() { + const { + id, + relativePath, + quality, + languages + } = this.props; + + const { + isSelectQualityModalOpen, + isSelectLanguageModalOpen, + isConfirmDeleteModalOpen + } = this.state; + + const showQualityPlaceholder = !quality; + + const showLanguagePlaceholder = !languages; + + return ( + + + {relativePath} + + + + + + + + + { + showLanguagePlaceholder && + + } + + { + !showLanguagePlaceholder && !!languages && + + } + + + + { + showQualityPlaceholder && + + } + + { + !showQualityPlaceholder && !!quality && + + } + + + + + + + - - - ); + + 1 : false} + real={quality ? quality.revision.real > 0 : false} + onModalClose={this.onSelectQualityModalClose} + /> + + + + ); + } + } MovieFileEditorRow.propTypes = { id: PropTypes.number.isRequired, + size: PropTypes.number.isRequired, relativePath: PropTypes.string.isRequired, - airDateUtc: PropTypes.string.isRequired, - language: PropTypes.object.isRequired, quality: PropTypes.object.isRequired, - isSelected: PropTypes.bool, - onSelectedChange: PropTypes.func.isRequired + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + mediaInfo: PropTypes.object.isRequired, + onDeletePress: PropTypes.func.isRequired }; export default MovieFileEditorRow; diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorTable.js b/frontend/src/MovieFile/Editor/MovieFileEditorTable.js new file mode 100644 index 000000000..6d3d37393 --- /dev/null +++ b/frontend/src/MovieFile/Editor/MovieFileEditorTable.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import MovieFileEditorTableContentConnector from './MovieFileEditorTableContentConnector'; + +function MovieFileEditorTable(props) { + const { + movieId + } = props; + + return ( + + ); +} + +MovieFileEditorTable.propTypes = { + movieId: PropTypes.number.isRequired +}; + +export default MovieFileEditorTable; diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorModalContent.css b/frontend/src/MovieFile/Editor/MovieFileEditorTableContent.css similarity index 53% rename from frontend/src/MovieFile/Editor/MovieFileEditorModalContent.css rename to frontend/src/MovieFile/Editor/MovieFileEditorTableContent.css index 49e946826..24834ef33 100644 --- a/frontend/src/MovieFile/Editor/MovieFileEditorModalContent.css +++ b/frontend/src/MovieFile/Editor/MovieFileEditorTableContent.css @@ -6,3 +6,9 @@ .selectInput { margin-left: 10px; } + +.blankpad { + padding-left:2em; + padding-top: 10px; + padding-bottom: 10px; +} diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js b/frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js new file mode 100644 index 000000000..eebb621ad --- /dev/null +++ b/frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js @@ -0,0 +1,86 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import MovieFileEditorRow from './MovieFileEditorRow'; +import styles from './MovieFileEditorTableContent.css'; + +const columns = [ + { + name: 'title', + label: 'Title', + isVisible: true + }, + { + name: 'mediainfo', + label: 'Media Info', + isVisible: true + }, + { + name: 'languages', + label: 'Languages', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'action', + label: 'Action', + isVisible: true + } +]; + +class MovieFileEditorTableContent extends Component { + + // + // Render + + render() { + const { + items + } = this.props; + + return ( +
+ { + !items.length && +
+ No movie files to manage. +
+ } + + { + !!items.length && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } + +
+ ); + } +} + +MovieFileEditorTableContent.propTypes = { + movieId: PropTypes.number, + isDeleting: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onDeletePress: PropTypes.func.isRequired +}; + +export default MovieFileEditorTableContent; diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorModalContentConnector.js b/frontend/src/MovieFile/Editor/MovieFileEditorTableContentConnector.js similarity index 55% rename from frontend/src/MovieFile/Editor/MovieFileEditorModalContentConnector.js rename to frontend/src/MovieFile/Editor/MovieFileEditorTableContentConnector.js index f1ef55c7c..8e0cf0290 100644 --- a/frontend/src/MovieFile/Editor/MovieFileEditorModalContentConnector.js +++ b/frontend/src/MovieFile/Editor/MovieFileEditorTableContentConnector.js @@ -6,26 +6,30 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import getQualities from 'Utilities/Quality/getQualities'; import createMovieSelector from 'Store/Selectors/createMovieSelector'; -import { deleteMovieFiles, updateMovieFiles } from 'Store/Actions/movieFileActions'; -import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; -import MovieFileEditorModalContent from './MovieFileEditorModalContent'; +import { deleteMovieFile, updateMovieFiles } from 'Store/Actions/movieFileActions'; +import { fetchQualityProfileSchema, fetchLanguages } from 'Store/Actions/settingsActions'; +import MovieFileEditorTableContent from './MovieFileEditorTableContent'; function createMapStateToProps() { return createSelector( (state) => state.movieFiles, - (state) => state.settings.qualityProfiles.schema, + (state) => state.settings.languages, + (state) => state.settings.qualityProfiles, createMovieSelector(), ( movieFiles, - qualityProfileSchema, - movie + languageProfiles, + qualityProfiles ) => { - const qualities = getQualities(qualityProfileSchema.items); + const languages = languageProfiles.items; + const qualities = getQualities(qualityProfiles.schema.items); return { items: movieFiles.items, isDeleting: movieFiles.isDeleting, isSaving: movieFiles.isSaving, + error: null, + languages, qualities }; } @@ -38,22 +42,27 @@ function createMapDispatchToProps(dispatch, props) { dispatch(fetchQualityProfileSchema()); }, + dispatchFetchLanguages(name, path) { + dispatch(fetchLanguages()); + }, + dispatchUpdateMovieFiles(updateProps) { dispatch(updateMovieFiles(updateProps)); }, - onDeletePress(episodeFileIds) { - dispatch(deleteMovieFiles({ episodeFileIds })); + onDeletePress(movieFileId) { + dispatch(deleteMovieFile(movieFileId)); } }; } -class MovieFileEditorModalContentConnector extends Component { +class MovieFileEditorTableContentConnector extends Component { // // Lifecycle componentDidMount() { + this.props.dispatchFetchLanguages(); this.props.dispatchFetchQualityProfileSchema(); } @@ -63,7 +72,14 @@ class MovieFileEditorModalContentConnector extends Component { // // Listeners - onQualityChange = (episodeFileIds, qualityId) => { + onLanguageChange = (movieFileIds, languageId) => { + const language = _.find(this.props.languages, { id: languageId }); + // TODO - Placeholder till we implement selection of multiple languages + const languages = [language]; + this.props.dispatchUpdateMovieFiles({ movieFileIds, languages }); + } + + onQualityChange = (movieFileIds, qualityId) => { const quality = { quality: _.find(this.props.qualities, { id: qualityId }), revision: { @@ -72,31 +88,34 @@ class MovieFileEditorModalContentConnector extends Component { } }; - this.props.dispatchUpdateMovieFiles({ episodeFileIds, quality }); + this.props.dispatchUpdateMovieFiles({ movieFileIds, quality }); } render() { const { + dispatchFetchLanguages, dispatchFetchQualityProfileSchema, dispatchUpdateMovieFiles, ...otherProps } = this.props; return ( - ); } } -MovieFileEditorModalContentConnector.propTypes = { +MovieFileEditorTableContentConnector.propTypes = { movieId: PropTypes.number.isRequired, languages: PropTypes.arrayOf(PropTypes.object).isRequired, qualities: PropTypes.arrayOf(PropTypes.object).isRequired, + dispatchFetchLanguages: PropTypes.func.isRequired, dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, dispatchUpdateMovieFiles: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, createMapDispatchToProps)(MovieFileEditorModalContentConnector); +export default connect(createMapStateToProps, createMapDispatchToProps)(MovieFileEditorTableContentConnector); diff --git a/frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.css b/frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.css new file mode 100644 index 000000000..941988144 --- /dev/null +++ b/frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.css @@ -0,0 +1,7 @@ +.placeholder { + display: inline-block; + margin: -8px 0; + width: 100%; + height: 25px; + border: 2px dashed $dangerColor; +} diff --git a/frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.js b/frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.js new file mode 100644 index 000000000..e2956a5ed --- /dev/null +++ b/frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.js @@ -0,0 +1,10 @@ +import React from 'react'; +import styles from './MovieFileRowCellPlaceholder.css'; + +function MovieFileRowCellPlaceholder() { + return ( + + ); +} + +export default MovieFileRowCellPlaceholder; diff --git a/frontend/src/MovieFile/Language/SelectLanguageModal.js b/frontend/src/MovieFile/Language/SelectLanguageModal.js new file mode 100644 index 000000000..938d26a6d --- /dev/null +++ b/frontend/src/MovieFile/Language/SelectLanguageModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectLanguageModalContentConnector from './SelectLanguageModalContentConnector'; + +class SelectLanguageModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectLanguageModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectLanguageModal; diff --git a/frontend/src/MovieFile/Language/SelectLanguageModalContent.js b/frontend/src/MovieFile/Language/SelectLanguageModalContent.js new file mode 100644 index 000000000..2f16d417b --- /dev/null +++ b/frontend/src/MovieFile/Language/SelectLanguageModalContent.js @@ -0,0 +1,87 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +function SelectLanguageModalContent(props) { + const { + languageId, + isFetching, + isPopulated, + error, + items, + onModalClose, + onLanguageSelect + } = props; + + const languageOptions = items.map(( language ) => { + return { + key: language.id, + value: language.name + }; + }); + + return ( + + + Manual Import - Select Language + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to load languages
+ } + + { + isPopulated && !error && +
+ + Language + + + +
+ } +
+ + + + +
+ ); +} + +SelectLanguageModalContent.propTypes = { + languageId: PropTypes.number.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onLanguageSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectLanguageModalContent; diff --git a/frontend/src/MovieFile/Language/SelectLanguageModalContentConnector.js b/frontend/src/MovieFile/Language/SelectLanguageModalContentConnector.js new file mode 100644 index 000000000..6625bc57a --- /dev/null +++ b/frontend/src/MovieFile/Language/SelectLanguageModalContentConnector.js @@ -0,0 +1,87 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchLanguages } from 'Store/Actions/settingsActions'; +import { updateMovieFiles } from 'Store/Actions/movieFileActions'; +import SelectLanguageModalContent from './SelectLanguageModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.languages, + (languages) => { + const { + isFetching, + isPopulated, + error, + items + } = languages; + + return { + isFetching, + isPopulated, + error, + items + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchLanguages: fetchLanguages, + dispatchupdateMovieFiles: updateMovieFiles +}; + +class SelectLanguageModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount = () => { + if (!this.props.isPopulated) { + this.props.dispatchFetchLanguages(); + } + } + + // + // Listeners + + onLanguageSelect = ({ value }) => { + const languageId = parseInt(value); + + const language = _.find(this.props.items, + (item) => item.id === languageId); + const languages = [language]; + const movieFileIds = this.props.ids; + + this.props.dispatchupdateMovieFiles({ movieFileIds, languages }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectLanguageModalContentConnector.propTypes = { + ids: PropTypes.arrayOf(PropTypes.number).isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + dispatchFetchLanguages: PropTypes.func.isRequired, + dispatchupdateMovieFiles: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectLanguageModalContentConnector); diff --git a/frontend/src/MovieFile/MediaInfoConnector.js b/frontend/src/MovieFile/MediaInfoConnector.js index 55dec12f7..ce955c8aa 100644 --- a/frontend/src/MovieFile/MediaInfoConnector.js +++ b/frontend/src/MovieFile/MediaInfoConnector.js @@ -6,10 +6,10 @@ import MediaInfo from './MediaInfo'; function createMapStateToProps() { return createSelector( createMovieFileSelector(), - (episodeFile) => { - if (episodeFile) { + (movieFile) => { + if (movieFile) { return { - ...episodeFile.mediaInfo + ...movieFile.mediaInfo }; } diff --git a/frontend/src/MovieFile/Quality/SelectQualityModal.js b/frontend/src/MovieFile/Quality/SelectQualityModal.js new file mode 100644 index 000000000..d3e31d2dd --- /dev/null +++ b/frontend/src/MovieFile/Quality/SelectQualityModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectQualityModalContentConnector from './SelectQualityModalContentConnector'; + +class SelectQualityModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectQualityModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectQualityModal; diff --git a/frontend/src/MovieFile/Quality/SelectQualityModalContent.js b/frontend/src/MovieFile/Quality/SelectQualityModalContent.js new file mode 100644 index 000000000..642e0433e --- /dev/null +++ b/frontend/src/MovieFile/Quality/SelectQualityModalContent.js @@ -0,0 +1,166 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +class SelectQualityModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + qualityId, + proper, + real + } = props; + + this.state = { + qualityId, + proper, + real + }; + } + + // + // Listeners + + onQualityChange = ({ value }) => { + this.setState({ qualityId: parseInt(value) }); + } + + onProperChange = ({ value }) => { + this.setState({ proper: value }); + } + + onRealChange = ({ value }) => { + this.setState({ real: value }); + } + + onQualitySelect = () => { + this.props.onQualitySelect(this.state); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + onModalClose + } = this.props; + + const { + qualityId, + proper, + real + } = this.state; + + const qualityOptions = items.map(({ id, name }) => { + return { + key: id, + value: name + }; + }); + + return ( + + + Manual Import - Select Quality + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to load qualities
+ } + + { + isPopulated && !error && +
+ + Quality + + + + + + Proper + + + + + + Real + + + +
+ } +
+ + + + + + +
+ ); + } +} + +SelectQualityModalContent.propTypes = { + qualityId: PropTypes.number.isRequired, + proper: PropTypes.bool.isRequired, + real: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onQualitySelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectQualityModalContent; diff --git a/frontend/src/MovieFile/Quality/SelectQualityModalContentConnector.js b/frontend/src/MovieFile/Quality/SelectQualityModalContentConnector.js new file mode 100644 index 000000000..7ead7d8ee --- /dev/null +++ b/frontend/src/MovieFile/Quality/SelectQualityModalContentConnector.js @@ -0,0 +1,97 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import getQualities from 'Utilities/Quality/getQualities'; +import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; +import { updateMovieFiles } from 'Store/Actions/movieFileActions'; +import SelectQualityModalContent from './SelectQualityModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.qualityProfiles, + (qualityProfiles) => { + const { + isSchemaFetching: isFetching, + isSchemaPopulated: isPopulated, + schemaError: error, + schema + } = qualityProfiles; + + return { + isFetching, + isPopulated, + error, + items: getQualities(schema.items) + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchQualityProfileSchema: fetchQualityProfileSchema, + dispatchupdateMovieFiles: updateMovieFiles +}; + +class SelectQualityModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount = () => { + if (!this.props.isPopulated) { + this.props.dispatchFetchQualityProfileSchema(); + } + } + + // + // Listeners + + onQualitySelect = ({ qualityId, proper, real }) => { + const quality = _.find(this.props.items, + (item) => item.id === qualityId); + + const revision = { + version: proper ? 2 : 1, + real: real ? 1 : 0 + }; + + const movieFileIds = this.props.ids; + + this.props.dispatchupdateMovieFiles({ + movieFileIds, + quality: { + quality, + revision + } + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectQualityModalContentConnector.propTypes = { + ids: PropTypes.arrayOf(PropTypes.number).isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, + dispatchupdateMovieFiles: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectQualityModalContentConnector); diff --git a/frontend/src/Store/Actions/blacklistActions.js b/frontend/src/Store/Actions/blacklistActions.js index a6eb4be86..31e3078a8 100644 --- a/frontend/src/Store/Actions/blacklistActions.js +++ b/frontend/src/Store/Actions/blacklistActions.js @@ -38,9 +38,16 @@ export const defaultState = { isSortable: true, isVisible: true }, + { + name: 'language', + label: 'Language', + isSortable: true, + isVisible: true + }, { name: 'quality', label: 'Quality', + isSortable: true, isVisible: true }, { diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index d1ad80b3c..105d7e001 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -42,11 +42,13 @@ export const defaultState = { { name: 'language', label: 'Language', - isVisible: false + isSortable: true, + isVisible: true }, { name: 'quality', label: 'Quality', + isSortable: true, isVisible: true }, { diff --git a/frontend/src/Store/Actions/movieFileActions.js b/frontend/src/Store/Actions/movieFileActions.js index a43e09be9..3c495969b 100644 --- a/frontend/src/Store/Actions/movieFileActions.js +++ b/frontend/src/Store/Actions/movieFileActions.js @@ -137,9 +137,10 @@ export const actionHandlers = handleThunks({ }, [UPDATE_MOVIE_FILES]: function(getState, payload, dispatch) { + const { movieFileIds, - language, + languages, quality } = payload; @@ -149,8 +150,8 @@ export const actionHandlers = handleThunks({ movieFileIds }; - if (language) { - data.language = language; + if (languages) { + data.languages = languages; } if (quality) { @@ -169,8 +170,8 @@ export const actionHandlers = handleThunks({ ...movieFileIds.map((id) => { const props = {}; - if (language) { - props.language = language; + if (languages) { + props.languages = languages; } if (quality) { diff --git a/frontend/src/Store/Actions/movieHistoryActions.js b/frontend/src/Store/Actions/movieHistoryActions.js index c0129a923..f323854e6 100644 --- a/frontend/src/Store/Actions/movieHistoryActions.js +++ b/frontend/src/Store/Actions/movieHistoryActions.js @@ -23,27 +23,27 @@ export const defaultState = { // // Actions Types -export const FETCH_SERIES_HISTORY = 'seriesHistory/fetchMovieHistory'; -export const CLEAR_SERIES_HISTORY = 'seriesHistory/clearMovieHistory'; -export const SERIES_HISTORY_MARK_AS_FAILED = 'seriesHistory/seriesHistoryMarkAsFailed'; +export const FETCH_MOVIE_HISTORY = 'movieHistory/fetchMovieHistory'; +export const CLEAR_MOVIE_HISTORY = 'movieHistory/clearMovieHistory'; +export const MOVIE_HISTORY_MARK_AS_FAILED = 'movieHistory/movieHistoryMarkAsFailed'; // // Action Creators -export const fetchMovieHistory = createThunk(FETCH_SERIES_HISTORY); -export const clearMovieHistory = createAction(CLEAR_SERIES_HISTORY); -export const seriesHistoryMarkAsFailed = createThunk(SERIES_HISTORY_MARK_AS_FAILED); +export const fetchMovieHistory = createThunk(FETCH_MOVIE_HISTORY); +export const clearMovieHistory = createAction(CLEAR_MOVIE_HISTORY); +export const movieHistoryMarkAsFailed = createThunk(MOVIE_HISTORY_MARK_AS_FAILED); // // Action Handlers export const actionHandlers = handleThunks({ - [FETCH_SERIES_HISTORY]: function(getState, payload, dispatch) { + [FETCH_MOVIE_HISTORY]: function(getState, payload, dispatch) { dispatch(set({ section, isFetching: true })); const promise = createAjaxRequest({ - url: '/history/series', + url: '/history/movie', data: payload }).request; @@ -70,11 +70,10 @@ export const actionHandlers = handleThunks({ }); }, - [SERIES_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) { + [MOVIE_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) { const { historyId, - seriesId, - seasonNumber + movieId } = payload; const promise = createAjaxRequest({ @@ -86,7 +85,7 @@ export const actionHandlers = handleThunks({ }).request; promise.done(() => { - dispatch(fetchMovieHistory({ seriesId, seasonNumber })); + dispatch(fetchMovieHistory({ movieId })); }); } }); @@ -96,7 +95,7 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ - [CLEAR_SERIES_HISTORY]: (state) => { + [CLEAR_MOVIE_HISTORY]: (state) => { return Object.assign({}, state, defaultState); } diff --git a/frontend/src/Store/Actions/movieTitlesActions.js b/frontend/src/Store/Actions/movieTitlesActions.js new file mode 100644 index 000000000..66b435bf3 --- /dev/null +++ b/frontend/src/Store/Actions/movieTitlesActions.js @@ -0,0 +1,74 @@ +import { batchActions } from 'redux-batched-actions'; +import createAjaxRequest from 'Utilities/createAjaxRequest'; +import { createThunk, handleThunks } from 'Store/thunks'; +import createHandleActions from './Creators/createHandleActions'; +import { set, update } from './baseActions'; + +// +// Variables + +export const section = 'movieTitles'; + +// +// State + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +// +// Actions Types + +export const FETCH_MOVIE_TITLES = 'movieTitles/fetchMovieTitles'; + +// +// Action Creators + +export const fetchMovieTitles = createThunk(FETCH_MOVIE_TITLES); + +// +// Action Handlers + +export const actionHandlers = handleThunks({ + + [FETCH_MOVIE_TITLES]: function(getState, payload, dispatch) { + dispatch(set({ section, isFetching: true })); + + const promise = createAjaxRequest({ + url: '/alttitle', + data: payload + }).request; + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + } +}); + +// +// Reducers + +export const reducers = createHandleActions({ + +}, defaultState, section); diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index d656e1572..f4de64e46 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -68,6 +68,12 @@ export const defaultState = { isSortable: true, isVisible: true }, + { + name: 'languages', + label: 'Languages', + isSortable: true, + isVisible: true + }, { name: 'quality', label: 'Quality', diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index 7e4688c92..5a88df5f6 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -11,8 +11,6 @@ import createHandleActions from './Creators/createHandleActions'; // Variables export const section = 'releases'; -export const episodeSection = 'releases.episode'; -export const seasonSection = 'releases.season'; let abortCurrentRequest = null; @@ -54,28 +52,6 @@ export const defaultState = { key: 'all', label: 'All', filters: [] - }, - { - key: 'season-pack', - label: 'Season Pack', - filters: [ - { - key: 'fullSeason', - value: true, - type: filterTypes.EQUAL - } - ] - }, - { - key: 'not-season-pack', - label: 'Not Season Pack', - filters: [ - { - key: 'fullSeason', - value: false, - type: filterTypes.EQUAL - } - ] } ], @@ -146,20 +122,13 @@ export const defaultState = { type: filterBuilderTypes.NUMBER } ], + selectedFilterKey: 'all' - episode: { - selectedFilterKey: 'all' - }, - - season: { - selectedFilterKey: 'season-pack' - } }; export const persistState = [ - 'releases.selectedFilterKey', - 'releases.episode.customFilters', - 'releases.season.customFilters' + 'releases.customFilters', + 'releases.selectedFilterKey' ]; // @@ -171,8 +140,7 @@ export const SET_RELEASES_SORT = 'releases/setReleasesSort'; export const CLEAR_RELEASES = 'releases/clearReleases'; export const GRAB_RELEASE = 'releases/grabRelease'; export const UPDATE_RELEASE = 'releases/updateRelease'; -export const SET_EPISODE_RELEASES_FILTER = 'releases/setEpisodeReleasesFilter'; -export const SET_SEASON_RELEASES_FILTER = 'releases/setSeasonReleasesFilter'; +export const SET_RELEASES_FILTER = 'releases/setMovieReleasesFilter'; // // Action Creators @@ -183,8 +151,7 @@ export const setReleasesSort = createAction(SET_RELEASES_SORT); export const clearReleases = createAction(CLEAR_RELEASES); export const grabRelease = createThunk(GRAB_RELEASE); export const updateRelease = createAction(UPDATE_RELEASE); -export const setEpisodeReleasesFilter = createAction(SET_EPISODE_RELEASES_FILTER); -export const setSeasonReleasesFilter = createAction(SET_SEASON_RELEASES_FILTER); +export const setReleasesFilter = createAction(SET_RELEASES_FILTER); // // Helpers @@ -248,13 +215,7 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ [CLEAR_RELEASES]: (state) => { - const { - episode, - season, - ...otherDefaultState - } = defaultState; - - return Object.assign({}, state, otherDefaultState); + return Object.assign({}, state, defaultState); }, [UPDATE_RELEASE]: (state, { payload }) => { @@ -276,8 +237,7 @@ export const reducers = createHandleActions({ return newState; }, - [SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section), - [SET_EPISODE_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(episodeSection), - [SET_SEASON_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(seasonSection) + [SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section), + [SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section) }, defaultState, section); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index ec22dad3c..57b5dce09 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -148,7 +148,7 @@ private IEnumerable GetMovieDecisions(List report } else { - //remoteMovie.DownloadAllowed = true; + remoteMovie.DownloadAllowed = true; decision = GetDecisionForReport(remoteMovie, searchCriteria); } diff --git a/src/Radarr.Api.V2/MovieFiles/MovieFileListResource.cs b/src/Radarr.Api.V2/MovieFiles/MovieFileListResource.cs index 38f6c338a..7ced9d024 100644 --- a/src/Radarr.Api.V2/MovieFiles/MovieFileListResource.cs +++ b/src/Radarr.Api.V2/MovieFiles/MovieFileListResource.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using NzbDrone.Core.Languages; using NzbDrone.Core.Qualities; namespace Radarr.Api.V2.MovieFiles @@ -6,6 +7,7 @@ namespace Radarr.Api.V2.MovieFiles public class MovieFileListResource { public List MovieFileIds { get; set; } + public List Languages { get; set; } public QualityModel Quality { get; set; } } } diff --git a/src/Radarr.Api.V2/MovieFiles/MovieFileModule.cs b/src/Radarr.Api.V2/MovieFiles/MovieFileModule.cs index 940860e5d..e9065a523 100644 --- a/src/Radarr.Api.V2/MovieFiles/MovieFileModule.cs +++ b/src/Radarr.Api.V2/MovieFiles/MovieFileModule.cs @@ -42,10 +42,10 @@ public MovieFileModule(IBroadcastSignalRMessage signalRBroadcaster, GetResourceById = GetMovieFile; GetResourceAll = GetMovieFiles; - UpdateResource = SetQuality; + UpdateResource = SetMovieFile; DeleteResource = DeleteMovieFile; - Put["/editor"] = movieFiles => SetQuality(); + Put["/editor"] = movieFiles => SetMovieFile(); Delete["/bulk"] = movieFiles => DeleteMovieFiles(); } @@ -92,14 +92,15 @@ private List GetMovieFiles() } } - private void SetQuality(MovieFileResource movieFileResource) + private void SetMovieFile(MovieFileResource movieFileResource) { var movieFile = _mediaFileService.GetMovie(movieFileResource.Id); movieFile.Quality = movieFileResource.Quality; + movieFile.Languages = movieFileResource.Languages; _mediaFileService.Update(movieFile); } - private Response SetQuality() + private Response SetMovieFile() { var resource = Request.Body.FromJson(); var movieFiles = _mediaFileService.GetMovies(resource.MovieFileIds); @@ -111,6 +112,11 @@ private Response SetQuality() { movieFile.Quality = resource.Quality; } + if (resource.Languages != null) + { + movieFile.Languages = resource.Languages; + } + } _mediaFileService.Update(movieFiles);