diff --git a/appveyor.yml b/appveyor.yml index 5b83d2da5..9224d3bd1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: '0.2.0.{build}' +version: '2.0.0.{build}' image: Visual Studio 2017 diff --git a/frontend/src/Components/Page/ErrorPage.js b/frontend/src/Components/Page/ErrorPage.js index b19ec0888..4f01a8a0f 100644 --- a/frontend/src/Components/Page/ErrorPage.js +++ b/frontend/src/Components/Page/ErrorPage.js @@ -11,6 +11,7 @@ function ErrorPage(props) { customFiltersError, tagsError, qualityProfilesError, + languagesError, uiSettingsError, systemStatusError } = props; @@ -27,6 +28,8 @@ function ErrorPage(props) { errorMessage = getErrorMessage(tagsError, 'Failed to load tags from API'); } else if (qualityProfilesError) { errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API'); + } else if (languagesError) { + errorMessage = getErrorMessage(languagesError, 'Failed to load languages from API'); } else if (uiSettingsError) { errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API'); } else if (systemStatusError) { @@ -53,6 +56,7 @@ ErrorPage.propTypes = { customFiltersError: PropTypes.object, tagsError: PropTypes.object, qualityProfilesError: PropTypes.object, + languagesError: PropTypes.object, uiSettingsError: PropTypes.object, systemStatusError: PropTypes.object }; diff --git a/frontend/src/Components/Page/Header/PageHeader.css b/frontend/src/Components/Page/Header/PageHeader.css index 5cd7656aa..46b95fde6 100644 --- a/frontend/src/Components/Page/Header/PageHeader.css +++ b/frontend/src/Components/Page/Header/PageHeader.css @@ -15,8 +15,9 @@ padding-left: 20px; } -.logoLink { - line-height: 0; +.logoFull { + width: 144px; + height: 48px; } .logo { diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js index 497287f87..1847d937f 100644 --- a/frontend/src/Components/Page/Header/PageHeader.js +++ b/frontend/src/Components/Page/Header/PageHeader.js @@ -45,19 +45,17 @@ class PageHeader extends Component { render() { const { - onSidebarToggle + onSidebarToggle, + isSmallScreen } = this.props; return (
- +
@@ -95,6 +93,7 @@ class PageHeader extends Component { PageHeader.propTypes = { onSidebarToggle: PropTypes.func.isRequired, + isSmallScreen: PropTypes.bool.isRequired, bindShortcut: PropTypes.func.isRequired }; diff --git a/frontend/src/Components/Page/Page.js b/frontend/src/Components/Page/Page.js index 4f871f864..2bb59c532 100644 --- a/frontend/src/Components/Page/Page.js +++ b/frontend/src/Components/Page/Page.js @@ -86,6 +86,7 @@ class Page extends Component {
diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index 340663da0..516548b75 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -8,7 +8,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchMovies } from 'Store/Actions/movieActions'; import { fetchTags } from 'Store/Actions/tagActions'; -import { fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; +import { fetchQualityProfiles, fetchUISettings, fetchLanguages } from 'Store/Actions/settingsActions'; import { fetchStatus } from 'Store/Actions/systemActions'; import ErrorPage from './ErrorPage'; import LoadingPage from './LoadingPage'; @@ -48,6 +48,7 @@ const selectIsPopulated = createSelector( (state) => state.tags.isPopulated, (state) => state.settings.ui.isPopulated, (state) => state.settings.qualityProfiles.isPopulated, + (state) => state.settings.languages.isPopulated, (state) => state.system.status.isPopulated, ( moviesIsPopulated, @@ -55,6 +56,7 @@ const selectIsPopulated = createSelector( tagsIsPopulated, uiSettingsIsPopulated, qualityProfilesIsPopulated, + languagesIsPopulated, systemStatusIsPopulated ) => { return ( @@ -63,6 +65,7 @@ const selectIsPopulated = createSelector( tagsIsPopulated && uiSettingsIsPopulated && qualityProfilesIsPopulated && + languagesIsPopulated && systemStatusIsPopulated ); } @@ -74,6 +77,7 @@ const selectErrors = createSelector( (state) => state.tags.error, (state) => state.settings.ui.error, (state) => state.settings.qualityProfiles.error, + (state) => state.settings.languages.error, (state) => state.system.status.error, ( moviesError, @@ -81,6 +85,7 @@ const selectErrors = createSelector( tagsError, uiSettingsError, qualityProfilesError, + languagesError, systemStatusError ) => { const hasError = !!( @@ -89,6 +94,7 @@ const selectErrors = createSelector( tagsError || uiSettingsError || qualityProfilesError || + languagesError || systemStatusError ); @@ -99,6 +105,7 @@ const selectErrors = createSelector( tagsError, uiSettingsError, qualityProfilesError, + languagesError, systemStatusError }; } @@ -143,6 +150,9 @@ function createMapDispatchToProps(dispatch, props) { dispatchFetchQualityProfiles() { dispatch(fetchQualityProfiles()); }, + dispatchFetchLanguages() { + dispatch(fetchLanguages()); + }, dispatchFetchUISettings() { dispatch(fetchUISettings()); }, @@ -177,6 +187,7 @@ class PageConnector extends Component { this.props.dispatchFetchCustomFilters(); this.props.dispatchFetchTags(); this.props.dispatchFetchQualityProfiles(); + this.props.dispatchFetchLanguages(); this.props.dispatchFetchUISettings(); this.props.dispatchFetchStatus(); } @@ -199,6 +210,7 @@ class PageConnector extends Component { dispatchFetchMovies, dispatchFetchTags, dispatchFetchQualityProfiles, + dispatchFetchLanguages, dispatchFetchUISettings, dispatchFetchStatus, ...otherProps @@ -236,6 +248,7 @@ PageConnector.propTypes = { dispatchFetchCustomFilters: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired, + dispatchFetchLanguages: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired, onSidebarVisibleChange: PropTypes.func.isRequired diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css index e6a0351af..d50f3a261 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css @@ -16,24 +16,21 @@ } .leftButtons, -.centerButtons, .rightButtons { display: flex; - flex: 1 0 33%; + flex: 1 0 50%; flex-wrap: wrap; } -.centerButtons { - justify-content: center; -} - .rightButtons { justify-content: flex-end; } -.importMode { +.importMode, +.bulkSelect { composes: select from '~Components/Form/SelectInput.css'; + margin-right: 10px; width: auto; } @@ -44,7 +41,6 @@ @media only screen and (max-width: $breakpointSmall) { .footer { .leftButtons, - .centerButtons, .rightButtons { flex-direction: column; } @@ -53,10 +49,6 @@ align-items: flex-start; } - .centerButtons { - align-items: center; - } - .rightButtons { align-items: flex-end; } diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js index fe8ce2ecc..71363f1f0 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -20,7 +20,9 @@ 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 SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; +import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; +import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; +import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal'; import InteractiveImportRow from './InteractiveImportRow'; import styles from './InteractiveImportModalContent.css'; @@ -32,8 +34,8 @@ const columns = [ isVisible: true }, { - name: 'series', - label: 'Series', + name: 'movie', + label: 'Movie', isSortable: true, isVisible: true }, @@ -69,6 +71,16 @@ const filterExistingFilesOptions = { NEW: 'new' }; +const importModeOptions = [ + { key: 'move', value: 'Move Files' }, + { key: 'copy', value: 'Copy Files' } +]; + +const SELECT = 'select'; +const MOVIE = 'movie'; +const LANGUAGE = 'language'; +const QUALITY = 'quality'; + class InteractiveImportModalContent extends Component { // @@ -83,7 +95,7 @@ class InteractiveImportModalContent extends Component { lastToggled: null, selectedState: {}, invalidRowsSelected: [], - isSelectSeriesModalOpen: false + selectModalOpen: null }; } @@ -122,9 +134,17 @@ class InteractiveImportModalContent extends Component { } onImportSelectedPress = () => { + const { + downloadId, + showImportMode, + importMode, + onImportSelectedPress + } = this.props; + const selected = this.getSelectedIds(); + const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode; - this.props.onImportSelectedPress(selected, this.props.importMode); + onImportSelectedPress(selected, finalImportMode); } onFilterExistingFilesChange = (value) => { @@ -135,12 +155,12 @@ class InteractiveImportModalContent extends Component { this.props.onImportModeChange(value); } - onSelectSeriesPress = () => { - this.setState({ isSelectSeriesModalOpen: true }); + onSelectModalSelect = ({ value }) => { + this.setState({ selectModalOpen: value }); } - onSelectSeriesModalClose = () => { - this.setState({ isSelectSeriesModalOpen: false }); + onSelectModalClose = () => { + this.setState({ selectModalOpen: null }); } // @@ -149,7 +169,7 @@ class InteractiveImportModalContent extends Component { render() { const { downloadId, - allowSeriesChange, + allowMovieChange, showFilterExistingFiles, showImportMode, filterExistingFiles, @@ -172,17 +192,25 @@ class InteractiveImportModalContent extends Component { allUnselected, selectedState, invalidRowsSelected, - isSelectSeriesModalOpen + selectModalOpen } = this.state; const selectedIds = this.getSelectedIds(); const errorMessage = getErrorMessage(error, 'Unable to load manual import items'); - const importModeOptions = [ - { key: 'move', value: 'Move Files' }, - { key: 'copy', value: 'Copy Files' } + const bulkSelectOptions = [ + { key: SELECT, value: 'Select...', disabled: true }, + { key: LANGUAGE, value: 'Select Language' }, + { key: QUALITY, value: 'Select Quality' } ]; + if (allowMovieChange) { + bulkSelectOptions.splice(1, 0, { + key: MOVIE, + value: 'Select Movie' + }); + } + return ( @@ -258,7 +286,7 @@ class InteractiveImportModalContent extends Component { key={item.id} isSelected={selectedState[item.id]} {...item} - allowSeriesChange={allowSeriesChange} + allowMovieChange={allowMovieChange} onSelectedChange={this.onSelectedChange} onValidRowChange={this.onValidRowChange} /> @@ -278,24 +306,25 @@ class InteractiveImportModalContent extends Component {
{ - !downloadId && showImportMode && + !downloadId && showImportMode ? + /> : + null } -
-
- { - allowSeriesChange && - - } +
@@ -318,10 +347,26 @@ class InteractiveImportModalContent extends Component {
- + + + +
); @@ -330,7 +375,7 @@ class InteractiveImportModalContent extends Component { InteractiveImportModalContent.propTypes = { downloadId: PropTypes.string, - allowSeriesChange: PropTypes.bool.isRequired, + allowMovieChange: PropTypes.bool.isRequired, showImportMode: PropTypes.bool.isRequired, showFilterExistingFiles: PropTypes.bool.isRequired, filterExistingFiles: PropTypes.bool.isRequired, @@ -352,7 +397,7 @@ InteractiveImportModalContent.propTypes = { }; InteractiveImportModalContent.defaultProps = { - allowSeriesChange: true, + allowMovieChange: true, showFilterExistingFiles: false, showImportMode: true, importMode: 'move' diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index 3a7b03fb6..a28919e29 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -19,11 +19,11 @@ function createMapStateToProps() { } const mapDispatchToProps = { - fetchInteractiveImportItems, - setInteractiveImportSort, - setInteractiveImportMode, - clearInteractiveImport, - executeCommand + dispatchFetchInteractiveImportItems: fetchInteractiveImportItems, + dispatchSetInteractiveImportSort: setInteractiveImportSort, + dispatchSetInteractiveImportMode: setInteractiveImportMode, + dispatchClearInteractiveImport: clearInteractiveImport, + dispatchExecuteCommand: executeCommand }; class InteractiveImportModalContentConnector extends Component { @@ -50,7 +50,7 @@ class InteractiveImportModalContentConnector extends Component { filterExistingFiles } = this.state; - this.props.fetchInteractiveImportItems({ + this.props.dispatchFetchInteractiveImportItems({ downloadId, folder, filterExistingFiles @@ -68,7 +68,7 @@ class InteractiveImportModalContentConnector extends Component { folder } = this.props; - this.props.fetchInteractiveImportItems({ + this.props.dispatchFetchInteractiveImportItems({ downloadId, folder, filterExistingFiles @@ -77,14 +77,14 @@ class InteractiveImportModalContentConnector extends Component { } componentWillUnmount() { - this.props.clearInteractiveImport(); + this.props.dispatchClearInteractiveImport(); } // // Listeners onSortPress = (sortKey, sortDirection) => { - this.props.setInteractiveImportSort({ sortKey, sortDirection }); + this.props.dispatchSetInteractiveImportSort({ sortKey, sortDirection }); } onFilterExistingFilesChange = (filterExistingFiles) => { @@ -92,7 +92,7 @@ class InteractiveImportModalContentConnector extends Component { } onImportModeChange = (importMode) => { - this.props.setInteractiveImportMode({ importMode }); + this.props.dispatchSetInteractiveImportMode({ importMode }); } onImportSelectedPress = (selected, importMode) => { @@ -103,25 +103,13 @@ class InteractiveImportModalContentConnector extends Component { if (isSelected) { const { - series, - seasonNumber, - episodes, + movie, quality, language } = item; - if (!series) { - this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' }); - return false; - } - - if (isNaN(seasonNumber)) { - this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' }); - return false; - } - - if (!episodes || !episodes.length) { - this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' }); + if (!movie) { + this.setState({ interactiveImportErrorMessage: 'Movie must be chosen for each selected file' }); return false; } @@ -138,8 +126,7 @@ class InteractiveImportModalContentConnector extends Component { files.push({ path: item.path, folderName: item.folderName, - seriesId: series.id, - episodeIds: _.map(episodes, 'id'), + movieId: movie.id, quality, language, downloadId: this.props.downloadId @@ -151,7 +138,7 @@ class InteractiveImportModalContentConnector extends Component { return; } - this.props.executeCommand({ + this.props.dispatchExecuteCommand({ name: commandNames.INTERACTIVE_IMPORT, files, importMode @@ -188,11 +175,11 @@ InteractiveImportModalContentConnector.propTypes = { folder: PropTypes.string, filterExistingFiles: PropTypes.bool.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, - fetchInteractiveImportItems: PropTypes.func.isRequired, - setInteractiveImportSort: PropTypes.func.isRequired, - clearInteractiveImport: PropTypes.func.isRequired, - setInteractiveImportMode: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired, + dispatchFetchInteractiveImportItems: PropTypes.func.isRequired, + dispatchSetInteractiveImportSort: PropTypes.func.isRequired, + dispatchSetInteractiveImportMode: PropTypes.func.isRequired, + dispatchClearInteractiveImport: PropTypes.func.isRequired, + dispatchExecuteCommand: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index 0f84b9a8c..090ef1fd9 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -9,9 +9,10 @@ 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 EpisodeLanguage from 'Episode/EpisodeLanguage'; -import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; +// import MovieLanguage from 'Movie/MovieLanguage'; +import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; +import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; import styles from './InteractiveImportRow.css'; @@ -24,21 +25,24 @@ class InteractiveImportRow extends Component { super(props, context); this.state = { - isSelectSeriesModalOpen: false, - isSelectQualityModalOpen: false + isSelectMovieModalOpen: false, + isSelectQualityModalOpen: false, + isSelectLanguageModalOpen: false }; } componentDidMount() { const { id, - series, - quality + movie, + quality, + language } = this.props; if ( - series && - quality + movie && + quality && + language ) { this.props.onSelectedChange({ id, value: true }); } @@ -47,23 +51,26 @@ class InteractiveImportRow extends Component { componentDidUpdate(prevProps) { const { id, - series, + movie, quality, + language, isSelected, onValidRowChange } = this.props; if ( - prevProps.series === series && + prevProps.movie === movie && prevProps.quality === quality && + prevProps.language === language && prevProps.isSelected === isSelected ) { return; } const isValid = !!( - series && - quality + movie && + quality && + language ); if (isSelected && !isValid) { @@ -90,16 +97,20 @@ class InteractiveImportRow extends Component { // // Listeners - onSelectSeriesPress = () => { - this.setState({ isSelectSeriesModalOpen: true }); + onSelectMoviePress = () => { + this.setState({ isSelectMovieModalOpen: true }); } onSelectQualityPress = () => { this.setState({ isSelectQualityModalOpen: true }); } - onSelectSeriesModalClose = (changed) => { - this.setState({ isSelectSeriesModalOpen: false }); + onSelectLanguagePress = () => { + this.setState({ isSelectLanguageModalOpen: true }); + } + + onSelectMovieModalClose = (changed) => { + this.setState({ isSelectMovieModalOpen: false }); this.selectRowAfterChange(changed); } @@ -108,16 +119,22 @@ class InteractiveImportRow extends Component { this.selectRowAfterChange(changed); } + onSelectLanguageModalClose = (changed) => { + this.setState({ isSelectLanguageModalOpen: false }); + this.selectRowAfterChange(changed); + } + // // Render render() { const { id, - allowSeriesChange, + allowMovieChange, relativePath, - series, + movie, quality, + language, size, rejections, isSelected, @@ -125,14 +142,16 @@ class InteractiveImportRow extends Component { } = this.props; const { - isSelectSeriesModalOpen, - isSelectQualityModalOpen + isSelectMovieModalOpen, + isSelectQualityModalOpen, + isSelectLanguageModalOpen } = this.state; - const seriesTitle = series ? series.title : ''; + const movieTitle = movie ? movie.title : ''; - const showSeriesPlaceholder = isSelected && !series; + const showMoviePlaceholder = isSelected && !movie; const showQualityPlaceholder = isSelected && !quality; + const showLanguagePlaceholder = isSelected && !language; return ( @@ -150,16 +169,18 @@ class InteractiveImportRow extends Component { { - showSeriesPlaceholder ? : seriesTitle + showMoviePlaceholder ? : movieTitle } { @@ -176,6 +197,25 @@ class InteractiveImportRow extends Component { } + + { + showLanguagePlaceholder && + + } + + {/* { + !showLanguagePlaceholder && !!language && + + } */} + + {formatBytes(size)} @@ -209,20 +249,27 @@ class InteractiveImportRow extends Component { } - 1 : false} real={quality ? quality.revision.real > 0 : false} onModalClose={this.onSelectQualityModalClose} /> + + ); } @@ -231,12 +278,11 @@ class InteractiveImportRow extends Component { InteractiveImportRow.propTypes = { id: PropTypes.number.isRequired, - allowSeriesChange: PropTypes.bool.isRequired, + allowMovieChange: PropTypes.bool.isRequired, relativePath: PropTypes.string.isRequired, - series: PropTypes.object, - seasonNumber: PropTypes.number, - episodes: PropTypes.arrayOf(PropTypes.object).isRequired, + movie: PropTypes.object, quality: PropTypes.object, + language: PropTypes.object, size: PropTypes.number.isRequired, rejections: PropTypes.arrayOf(PropTypes.object).isRequired, isSelected: PropTypes.bool, @@ -244,8 +290,4 @@ InteractiveImportRow.propTypes = { onValidRowChange: PropTypes.func.isRequired }; -InteractiveImportRow.defaultProps = { - episodes: [] -}; - export default InteractiveImportRow; diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModal.js b/frontend/src/InteractiveImport/Language/SelectLanguageModal.js new file mode 100644 index 000000000..938d26a6d --- /dev/null +++ b/frontend/src/InteractiveImport/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/InteractiveImport/Language/SelectLanguageModalContent.js b/frontend/src/InteractiveImport/Language/SelectLanguageModalContent.js new file mode 100644 index 000000000..2f16d417b --- /dev/null +++ b/frontend/src/InteractiveImport/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/InteractiveImport/Language/SelectLanguageModalContentConnector.js b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js new file mode 100644 index 000000000..846c6acc9 --- /dev/null +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js @@ -0,0 +1,88 @@ +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 { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; +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, + dispatchUpdateInteractiveImportItems: updateInteractiveImportItems +}; + +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); + + this.props.dispatchUpdateInteractiveImportItems({ + ids: this.props.ids, + language + }); + + 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, + dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectLanguageModalContentConnector); diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModal.js b/frontend/src/InteractiveImport/Movie/SelectMovieModal.js similarity index 68% rename from frontend/src/InteractiveImport/Series/SelectSeriesModal.js rename to frontend/src/InteractiveImport/Movie/SelectMovieModal.js index 1a1ceffca..a2ef71718 100644 --- a/frontend/src/InteractiveImport/Series/SelectSeriesModal.js +++ b/frontend/src/InteractiveImport/Movie/SelectMovieModal.js @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Modal from 'Components/Modal/Modal'; -import SelectSeriesModalContentConnector from './SelectSeriesModalContentConnector'; +import SelectMovieModalContentConnector from './SelectMovieModalContentConnector'; -class SelectSeriesModal extends Component { +class SelectMovieModal extends Component { // // Render @@ -20,7 +20,7 @@ class SelectSeriesModal extends Component { isOpen={isOpen} onModalClose={onModalClose} > - @@ -29,9 +29,9 @@ class SelectSeriesModal extends Component { } } -SelectSeriesModal.propTypes = { +SelectMovieModal.propTypes = { isOpen: PropTypes.bool.isRequired, onModalClose: PropTypes.func.isRequired }; -export default SelectSeriesModal; +export default SelectMovieModal; diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.css b/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.css similarity index 100% rename from frontend/src/InteractiveImport/Series/SelectSeriesModalContent.css rename to frontend/src/InteractiveImport/Movie/SelectMovieModalContent.css diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.js b/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.js similarity index 85% rename from frontend/src/InteractiveImport/Series/SelectSeriesModalContent.js rename to frontend/src/InteractiveImport/Movie/SelectMovieModalContent.js index 6c4c75255..2bae22307 100644 --- a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.js +++ b/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.js @@ -8,10 +8,10 @@ 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 SelectSeriesRow from './SelectSeriesRow'; -import styles from './SelectSeriesModalContent.css'; +import SelectMovieRow from './SelectMovieRow'; +import styles from './SelectMovieModalContent.css'; -class SelectSeriesModalContent extends Component { +class SelectMovieModalContent extends Component { // // Lifecycle @@ -46,7 +46,7 @@ class SelectSeriesModalContent extends Component { return ( - Manual Import - Select Series + Manual Import - Select Movie { return item.title.toLowerCase().includes(filter) ? ( - { - const series = _.find(this.props.items, { id: seriesId }); + onMovieSelect = (movieId) => { + const { + ids, + items, + dispatchUpdateInteractiveImportItem, + onModalClose + } = this.props; - this.props.ids.forEach((id) => { - this.props.updateInteractiveImportItem({ + const movie = items.find((s) => s.id === movieId); + + ids.forEach((id) => { + dispatchUpdateInteractiveImportItem({ id, - series, - seasonNumber: undefined, - episodes: [] + movie }); }); - this.props.onModalClose(true); + onModalClose(true); } // @@ -57,7 +61,7 @@ class SelectSeriesModalContentConnector extends Component { render() { return ( - @@ -65,11 +69,11 @@ class SelectSeriesModalContentConnector extends Component { } } -SelectSeriesModalContentConnector.propTypes = { +SelectMovieModalContentConnector.propTypes = { ids: PropTypes.arrayOf(PropTypes.number).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, - updateInteractiveImportItem: PropTypes.func.isRequired, + dispatchUpdateInteractiveImportItem: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; -export default connect(createMapStateToProps, mapDispatchToProps)(SelectSeriesModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(SelectMovieModalContentConnector); diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesRow.css b/frontend/src/InteractiveImport/Movie/SelectMovieRow.css similarity index 85% rename from frontend/src/InteractiveImport/Series/SelectSeriesRow.css rename to frontend/src/InteractiveImport/Movie/SelectMovieRow.css index f2573d585..f84778bb5 100644 --- a/frontend/src/InteractiveImport/Series/SelectSeriesRow.css +++ b/frontend/src/InteractiveImport/Movie/SelectMovieRow.css @@ -1,4 +1,4 @@ -.series { +.movie { padding: 8px; border-bottom: 1px solid $borderColor; } diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesRow.js b/frontend/src/InteractiveImport/Movie/SelectMovieRow.js similarity index 73% rename from frontend/src/InteractiveImport/Series/SelectSeriesRow.js rename to frontend/src/InteractiveImport/Movie/SelectMovieRow.js index 48ba77094..b48ec7992 100644 --- a/frontend/src/InteractiveImport/Series/SelectSeriesRow.js +++ b/frontend/src/InteractiveImport/Movie/SelectMovieRow.js @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Link from 'Components/Link/Link'; -import styles from './SelectSeriesRow.css'; +import styles from './SelectMovieRow.css'; -class SelectSeriesRow extends Component { +class SelectMovieRow extends Component { // // Listeners @@ -18,7 +18,7 @@ class SelectSeriesRow extends Component { render() { return ( @@ -28,10 +28,10 @@ class SelectSeriesRow extends Component { } } -SelectSeriesRow.propTypes = { +SelectMovieRow.propTypes = { id: PropTypes.number.isRequired, title: PropTypes.string.isRequired, onMovieSelect: PropTypes.func.isRequired }; -export default SelectSeriesRow; +export default SelectMovieRow; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js b/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js index 20a49c768..1cf55cde6 100644 --- a/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import getQualities from 'Utilities/Quality/getQualities'; import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; -import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions'; import SelectQualityModalContent from './SelectQualityModalContent'; function createMapStateToProps() { @@ -30,8 +30,8 @@ function createMapStateToProps() { } const mapDispatchToProps = { - fetchQualityProfileSchema, - updateInteractiveImportItem + dispatchFetchQualityProfileSchema: fetchQualityProfileSchema, + dispatchUpdateInteractiveImportItems: updateInteractiveImportItems }; class SelectQualityModalContentConnector extends Component { @@ -41,7 +41,7 @@ class SelectQualityModalContentConnector extends Component { componentDidMount = () => { if (!this.props.isPopulated) { - this.props.fetchQualityProfileSchema(); + this.props.dispatchFetchQualityProfileSchema(); } } @@ -57,8 +57,8 @@ class SelectQualityModalContentConnector extends Component { real: real ? 1 : 0 }; - this.props.updateInteractiveImportItem({ - id: this.props.id, + this.props.dispatchUpdateInteractiveImportItems({ + ids: this.props.ids, quality: { quality, revision @@ -82,13 +82,13 @@ class SelectQualityModalContentConnector extends Component { } SelectQualityModalContentConnector.propTypes = { - id: PropTypes.number.isRequired, + ids: PropTypes.arrayOf(PropTypes.number).isRequired, isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, - fetchQualityProfileSchema: PropTypes.func.isRequired, - updateInteractiveImportItem: PropTypes.func.isRequired, + dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, + dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/Movie/Index/MovieIndex.js b/frontend/src/Movie/Index/MovieIndex.js index 7e82c807e..7e714db51 100644 --- a/frontend/src/Movie/Index/MovieIndex.js +++ b/frontend/src/Movie/Index/MovieIndex.js @@ -21,6 +21,7 @@ import MovieIndexFilterMenu from './Menus/MovieIndexFilterMenu'; import MovieIndexSortMenu from './Menus/MovieIndexSortMenu'; import MovieIndexViewMenu from './Menus/MovieIndexViewMenu'; import MovieIndexFooterConnector from './MovieIndexFooterConnector'; +import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import styles from './MovieIndex.css'; function getViewComponent(view) { @@ -49,6 +50,7 @@ class MovieIndex extends Component { jumpToCharacter: null, isPosterOptionsModalOpen: false, isOverviewOptionsModalOpen: false, + isInteractiveImportModalOpen: false, isRendered: false }; } @@ -137,6 +139,14 @@ class MovieIndex extends Component { this.setState({ isOverviewOptionsModalOpen: false }); } + onInteractiveImportPress = () => { + this.setState({ isInteractiveImportModalOpen: true }); + } + + onInteractiveImportModalClose = () => { + this.setState({ isInteractiveImportModalOpen: false }); + } + onJumpBarItemPress = (jumpToCharacter) => { this.setState({ jumpToCharacter }); } @@ -195,6 +205,7 @@ class MovieIndex extends Component { jumpToCharacter, isPosterOptionsModalOpen, isOverviewOptionsModalOpen, + isInteractiveImportModalOpen, isRendered } = this.state; @@ -223,6 +234,28 @@ class MovieIndex extends Component { onPress={onRssSyncPress} /> + + + + + + + + + + + + ); } diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.css b/frontend/src/Movie/Index/Table/MovieIndexRow.css index 571bb65e5..a75849059 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.css +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.css @@ -24,6 +24,7 @@ .added, .inCinemas, +.physicalRelease, .genres { composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js index e390f251d..9b2d4b6d1 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js @@ -92,10 +92,12 @@ class EditQualityProfileModalContent extends Component { isSaving, saveError, qualities, + languages, item, isInUse, onInputChange, onCutoffChange, + onLanguageChange, onSavePress, onModalClose, onDeleteQualityProfilePress, @@ -105,10 +107,14 @@ class EditQualityProfileModalContent extends Component { const { id, name, + upgradeAllowed, cutoff, + language, items } = item; + const languageId = language.value.id; + return ( - Cutoff + Upgrades Allowed + + + + + + { + upgradeAllowed.value && + + + Upgrade Until + + + + + } + + + + Language
@@ -197,10 +235,10 @@ class EditQualityProfileModalContent extends Component { > { - id && + id ?
-
+
: + null }