1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-09-11 20:12:41 +02:00

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
This commit is contained in:
devbrian 2019-07-06 08:47:11 -05:00 committed by Qstick
parent 06b1c03053
commit 12fba024f0
60 changed files with 1565 additions and 821 deletions

View File

@ -6,6 +6,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import BlacklistDetailsModal from './BlacklistDetailsModal'; import BlacklistDetailsModal from './BlacklistDetailsModal';
import styles from './BlacklistRow.css'; import styles from './BlacklistRow.css';
@ -42,6 +43,7 @@ class BlacklistRow extends Component {
movie, movie,
sourceTitle, sourceTitle,
quality, quality,
languages,
date, date,
protocol, protocol,
indexer, indexer,
@ -82,6 +84,16 @@ class BlacklistRow extends Component {
); );
} }
if (name === 'language') {
return (
<TableRowCell key={name}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') { if (name === 'quality') {
return ( return (
<TableRowCell <TableRowCell
@ -159,6 +171,7 @@ BlacklistRow.propTypes = {
movie: PropTypes.object.isRequired, movie: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
indexer: PropTypes.string, indexer: PropTypes.string,

View File

@ -6,6 +6,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import HistoryEventTypeCell from './HistoryEventTypeCell'; import HistoryEventTypeCell from './HistoryEventTypeCell';
import HistoryDetailsModal from './Details/HistoryDetailsModal'; import HistoryDetailsModal from './Details/HistoryDetailsModal';
@ -52,6 +53,7 @@ class HistoryRow extends Component {
const { const {
movie, movie,
quality, quality,
languages,
qualityCutoffNotMet, qualityCutoffNotMet,
eventType, eventType,
sourceTitle, sourceTitle,
@ -102,6 +104,16 @@ class HistoryRow extends Component {
); );
} }
if (name === 'language') {
return (
<TableRowCell key={name}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') { if (name === 'quality') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
@ -193,8 +205,7 @@ class HistoryRow extends Component {
HistoryRow.propTypes = { HistoryRow.propTypes = {
movieId: PropTypes.number, movieId: PropTypes.number,
movie: PropTypes.object.isRequired, movie: PropTypes.object.isRequired,
language: PropTypes.object.isRequired, languages: PropTypes.arrayOf(PropTypes.object).isRequired,
languageCutoffNotMet: PropTypes.bool.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired, eventType: PropTypes.string.isRequired,

View File

@ -10,6 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
import MovieLanguage from 'Movie/MovieLanguage';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import QueueStatusCell from './QueueStatusCell'; import QueueStatusCell from './QueueStatusCell';
@ -69,6 +70,7 @@ class QueueRow extends Component {
errorMessage, errorMessage,
movie, movie,
quality, quality,
languages,
protocol, protocol,
indexer, indexer,
outputPath, outputPath,
@ -145,6 +147,16 @@ class QueueRow extends Component {
); );
} }
if (name === 'language') {
return (
<TableRowCell key={name}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') { if (name === 'quality') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
@ -297,6 +309,7 @@ QueueRow.propTypes = {
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
movie: PropTypes.object.isRequired, movie: PropTypes.object.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
indexer: PropTypes.string, indexer: PropTypes.string,
outputPath: PropTypes.string, outputPath: PropTypes.string,

View File

@ -128,7 +128,7 @@ class InteractiveImportModalContentConnector extends Component {
folderName: item.folderName, folderName: item.folderName,
movieId: movie.id, movieId: movie.id,
quality, quality,
language, languages: [language],
downloadId: this.props.downloadId downloadId: this.props.downloadId
}); });
} }

View File

@ -7,8 +7,6 @@
.quality, .quality,
.language { .language {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
text-align: center;
} }
.label { .label {

View File

@ -9,7 +9,7 @@ import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
// import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal'; import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
@ -152,7 +152,8 @@ class InteractiveImportRow extends Component {
const showMoviePlaceholder = isSelected && !movie; const showMoviePlaceholder = isSelected && !movie;
const showQualityPlaceholder = isSelected && !quality; const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !language; const showLanguagePlaceholder = isSelected && !language;
// TODO - Placeholder till we implement selection of multiple languages
const languages = [language];
return ( return (
<TableRow> <TableRow>
<TableSelectCell <TableSelectCell
@ -207,13 +208,13 @@ class InteractiveImportRow extends Component {
<InteractiveImportRowCellPlaceholder /> <InteractiveImportRowCellPlaceholder />
} }
{/* { {
!showLanguagePlaceholder && !!language && !showLanguagePlaceholder && !!language &&
<MovieLanguage <MovieLanguage
className={styles.label} className={styles.label}
language={language} languages={languages}
/> />
} */} }
</TableRowCellButton> </TableRowCellButton>
<TableRowCell> <TableRowCell>

View File

@ -9,7 +9,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRow from './InteractiveSearchRow'; import InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveSearch.css'; import styles from './InteractiveSearchContent.css';
const columns = [ const columns = [
{ {
@ -48,6 +48,12 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'languageWeight',
label: 'Language',
isSortable: true,
isVisible: true
},
{ {
name: 'qualityWeight', name: 'qualityWeight',
label: 'Quality', label: 'Quality',
@ -70,7 +76,7 @@ const columns = [
} }
]; ];
function InteractiveSearch(props) { function InteractiveSearchContent(props) {
const { const {
searchPayload, searchPayload,
isFetching, isFetching,
@ -83,7 +89,6 @@ function InteractiveSearch(props) {
customFilters, customFilters,
sortKey, sortKey,
sortDirection, sortDirection,
type,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
onSortPress, onSortPress,
@ -101,7 +106,6 @@ function InteractiveSearch(props) {
customFilters={customFilters} customFilters={customFilters}
buttonComponent={PageMenuButton} buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector} filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
filterModalConnectorComponentProps={{ type }}
onFilterSelect={onFilterSelect} onFilterSelect={onFilterSelect}
/> />
</div> </div>
@ -169,7 +173,7 @@ function InteractiveSearch(props) {
); );
} }
InteractiveSearch.propTypes = { InteractiveSearchContent.propTypes = {
searchPayload: PropTypes.object.isRequired, searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
@ -181,7 +185,6 @@ InteractiveSearch.propTypes = {
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, sortKey: PropTypes.string,
sortDirection: PropTypes.string, sortDirection: PropTypes.string,
type: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
@ -189,4 +192,4 @@ InteractiveSearch.propTypes = {
onGrabPress: PropTypes.func.isRequired onGrabPress: PropTypes.func.isRequired
}; };
export default InteractiveSearch; export default InteractiveSearchContent;

View File

@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
import * as releaseActions from 'Store/Actions/releaseActions'; import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearch from './InteractiveSearch'; import InteractiveSearchContent from './InteractiveSearchContent';
function createMapStateToProps(appState, { type }) { function createMapStateToProps(appState) {
return createSelector( return createSelector(
(state) => state.releases.items.length, (state) => state.releases.items.length,
createClientSideCollectionSelector('releases', `releases.${type}`), createClientSideCollectionSelector('releases'),
createUISettingsSelector(), createUISettingsSelector(),
(totalReleasesCount, releases, uiSettings) => { (totalReleasesCount, releases, uiSettings) => {
return { return {
@ -29,15 +29,16 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(releaseActions.fetchReleases(payload)); dispatch(releaseActions.fetchReleases(payload));
}, },
dispatchClearReleases(payload) {
dispatch(releaseActions.clearReleases(payload));
},
onSortPress(sortKey, sortDirection) { onSortPress(sortKey, sortDirection) {
dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection })); dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection }));
}, },
onFilterSelect(selectedFilterKey) { onFilterSelect(selectedFilterKey) {
const action = props.type === 'episode' ? const action = releaseActions.setReleasesFilter;
releaseActions.setEpisodeReleasesFilter :
releaseActions.setSeasonReleasesFilter;
dispatch(action({ selectedFilterKey })); dispatch(action({ selectedFilterKey }));
}, },
@ -47,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) {
}; };
} }
class InteractiveSearchConnector extends Component { class InteractiveSearchContentConnector extends Component {
// //
// Lifecycle // Lifecycle
@ -61,7 +62,6 @@ class InteractiveSearchConnector extends Component {
// If search results are not yet isPopulated fetch them, // If search results are not yet isPopulated fetch them,
// otherwise re-show the existing props. // otherwise re-show the existing props.
if (!isPopulated) { if (!isPopulated) {
dispatchFetchReleases(searchPayload); dispatchFetchReleases(searchPayload);
} }
@ -73,22 +73,24 @@ class InteractiveSearchConnector extends Component {
render() { render() {
const { const {
dispatchFetchReleases, dispatchFetchReleases,
dispatchClearReleases,
...otherProps ...otherProps
} = this.props; } = this.props;
return ( return (
<InteractiveSearch <InteractiveSearchContent
{...otherProps} {...otherProps}
/> />
); );
} }
} }
InteractiveSearchConnector.propTypes = { InteractiveSearchContentConnector.propTypes = {
searchPayload: PropTypes.object.isRequired, searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.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);

View File

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { setEpisodeReleasesFilter, setSeasonReleasesFilter } from 'Store/Actions/releaseActions'; import { setReleasesFilter } from 'Store/Actions/releaseActions';
import FilterModal from 'Components/Filter/FilterModal'; import FilterModal from 'Components/Filter/FilterModal';
function createMapStateToProps() { function createMapStateToProps() {
@ -20,10 +20,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
dispatchSetFilter(payload) { dispatchSetFilter(payload) {
const action = props.type === 'episode' ? const action = setReleasesFilter;
setEpisodeReleasesFilter:
setSeasonReleasesFilter;
dispatch(action(payload)); dispatch(action(payload));
} }
}; };

View File

@ -1,13 +1,10 @@
.title { .quality,
.language {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
} }
.quality { .language {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; width: 100px;
text-align: center;
} }
.rejected, .rejected,

View File

@ -11,10 +11,11 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import EpisodeQuality from 'Episode/EpisodeQuality';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import Peers from './Peers'; import Peers from './Peers';
import styles from './InteractiveSearchRow.css'; import styles from './InteractiveSearchRow.css';
import MovieQuality from 'Movie/MovieQuality';
import MovieLanguage from 'Movie/MovieLanguage';
function getDownloadIcon(isGrabbing, isGrabbed, grabError) { function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) { if (isGrabbing) {
@ -111,6 +112,7 @@ class InteractiveSearchRow extends Component {
seeders, seeders,
leechers, leechers,
quality, quality,
languages,
rejections, rejections,
downloadAllowed, downloadAllowed,
isGrabbing, isGrabbing,
@ -159,8 +161,14 @@ class InteractiveSearchRow extends Component {
} }
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.language}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
<TableRowCell className={styles.quality}> <TableRowCell className={styles.quality}>
<EpisodeQuality <MovieQuality
quality={quality} quality={quality}
/> />
</TableRowCell> </TableRowCell>
@ -199,6 +207,7 @@ class InteractiveSearchRow extends Component {
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)} name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError ? kinds.DANGER : kinds.DEFAULT} kind={grabError ? kinds.DANGER : kinds.DEFAULT}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)} title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing} isSpinning={isGrabbing}
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress} onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
/> />
@ -208,7 +217,7 @@ class InteractiveSearchRow extends Component {
isOpen={this.state.isConfirmGrabModalOpen} isOpen={this.state.isConfirmGrabModalOpen}
kind={kinds.WARNING} kind={kinds.WARNING}
title="Grab Release" 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" confirmLabel="Grab"
onConfirm={this.onGrabConfirm} onConfirm={this.onGrabConfirm}
onCancel={this.onGrabCancel} onCancel={this.onGrabCancel}
@ -233,6 +242,7 @@ InteractiveSearchRow.propTypes = {
seeders: PropTypes.number, seeders: PropTypes.number,
leechers: PropTypes.number, leechers: PropTypes.number,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
rejections: PropTypes.arrayOf(PropTypes.string).isRequired, rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
downloadAllowed: PropTypes.bool.isRequired, downloadAllowed: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired,

View File

@ -0,0 +1,16 @@
import React from 'react';
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
function InteractiveSearchTable(props) {
return (
<InteractiveSearchContentConnector
searchPayload={props}
/>
);
}
InteractiveSearchTable.propTypes = {
};
export default InteractiveSearchTable;

View File

@ -22,15 +22,17 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import MovieFileEditorModal from 'MovieFile/Editor/MovieFileEditorModal'; import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; 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 MovieAlternateTitles from './MovieAlternateTitles';
import MovieDetailsLinks from './MovieDetailsLinks'; import MovieDetailsLinks from './MovieDetailsLinks';
import InteractiveSearchTable from '../../InteractiveSearch/InteractiveSearchTable';
// import MovieTagsConnector from './MovieTagsConnector'; // import MovieTagsConnector from './MovieTagsConnector';
import styles from './MovieDetails.css'; import styles from './MovieDetails.css';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
@ -68,7 +70,6 @@ class MovieDetails extends Component {
isManageEpisodesOpen: false, isManageEpisodesOpen: false,
isEditMovieModalOpen: false, isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false, isDeleteMovieModalOpen: false,
isMovieHistoryModalOpen: false,
isInteractiveImportModalOpen: false, isInteractiveImportModalOpen: false,
allExpanded: false, allExpanded: false,
allCollapsed: false, allCollapsed: false,
@ -123,14 +124,6 @@ class MovieDetails extends Component {
this.setState({ isDeleteMovieModalOpen: false }); this.setState({ isDeleteMovieModalOpen: false });
} }
onMovieHistoryPress = () => {
this.setState({ isMovieHistoryModalOpen: true });
}
onMovieHistoryModalClose = () => {
this.setState({ isMovieHistoryModalOpen: false });
}
onExpandAllPress = () => { onExpandAllPress = () => {
const { const {
allExpanded, allExpanded,
@ -195,10 +188,8 @@ class MovieDetails extends Component {
const { const {
isOrganizeModalOpen, isOrganizeModalOpen,
isManageEpisodesOpen,
isEditMovieModalOpen, isEditMovieModalOpen,
isDeleteMovieModalOpen, isDeleteMovieModalOpen,
isMovieHistoryModalOpen,
isInteractiveImportModalOpen, isInteractiveImportModalOpen,
overviewHeight overviewHeight
} = this.state; } = this.state;
@ -488,19 +479,27 @@ class MovieDetails extends Component {
</TabList> </TabList>
<TabPanel> <TabPanel>
<h2>Any content 1</h2> <MovieHistoryTable
movieId={id}
/>
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<h2>Any content 2</h2> <InteractiveSearchTable
movieId={id}
/>
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<h2>Any content 3</h2> <MovieFileEditorTable
movieId={id}
/>
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<h2>Any content 4</h2> <MovieTitlesTable
movieId={id}
/>
</TabPanel> </TabPanel>
</Tabs> </Tabs>
@ -512,18 +511,6 @@ class MovieDetails extends Component {
onModalClose={this.onOrganizeModalClose} onModalClose={this.onOrganizeModalClose}
/> />
<MovieFileEditorModal
isOpen={isManageEpisodesOpen}
movieId={id}
onModalClose={this.onManageEpisodesModalClose}
/>
<MovieHistoryModal
isOpen={isMovieHistoryModalOpen}
movieId={id}
onModalClose={this.onMovieHistoryModalClose}
/>
<EditMovieModalConnector <EditMovieModalConnector
isOpen={isEditMovieModalOpen} isOpen={isEditMovieModalOpen}
movieId={id} movieId={id}

View File

@ -10,6 +10,7 @@ import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import { fetchMovieFiles, clearMovieFiles } from 'Store/Actions/movieFileActions'; import { fetchMovieFiles, clearMovieFiles } from 'Store/Actions/movieFileActions';
import { toggleMovieMonitored } from 'Store/Actions/movieActions'; import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
import { clearReleases } from 'Store/Actions/releaseActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import MovieDetails from './MovieDetails'; import MovieDetails from './MovieDetails';
@ -108,6 +109,7 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
fetchMovieFiles, fetchMovieFiles,
clearMovieFiles, clearMovieFiles,
clearReleases,
toggleMovieMonitored, toggleMovieMonitored,
fetchQueueDetails, fetchQueueDetails,
clearQueueDetails, clearQueueDetails,
@ -169,6 +171,7 @@ class MovieDetailsConnector extends Component {
unpopulate = () => { unpopulate = () => {
this.props.clearMovieFiles(); this.props.clearMovieFiles();
this.props.clearQueueDetails(); this.props.clearQueueDetails();
this.props.clearReleases();
} }
// //
@ -220,6 +223,7 @@ MovieDetailsConnector.propTypes = {
isRenamingMovie: PropTypes.bool.isRequired, isRenamingMovie: PropTypes.bool.isRequired,
fetchMovieFiles: PropTypes.func.isRequired, fetchMovieFiles: PropTypes.func.isRequired,
clearMovieFiles: PropTypes.func.isRequired, clearMovieFiles: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired,
toggleMovieMonitored: PropTypes.func.isRequired, toggleMovieMonitored: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired, fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired, clearQueueDetails: PropTypes.func.isRequired,

View File

@ -5,12 +5,12 @@
.downloaded { .downloaded {
padding-left: 2px; padding-left: 2px;
border-left: 4px solid $dangerColor; border-left: 4px solid $successColor;
} }
.unaired { .unreleased {
padding-left: 2px; padding-left: 2px;
border-left: 4px solid $gray; border-left: 4px solid $primaryColor;
} }
.unmonitored { .unmonitored {

View File

@ -18,7 +18,7 @@ function getMovieStatus(hasFile, isMonitored, inCinemas) {
return 'Missing'; return 'Missing';
} }
return 'Unaired'; return 'Unreleased';
} }
function MovieStatusLabel(props) { function MovieStatusLabel(props) {

View File

@ -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 (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<MovieHistoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieHistoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModal;

View File

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
History
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load history.</div>
}
{
isPopulated && !hasItems && !error &&
<div>No history.</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
fullSeries={fullSeries}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
);
}
}
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;

View File

@ -9,6 +9,7 @@ import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
import MovieLanguage from 'Movie/MovieLanguage';
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import styles from './MovieHistoryRow.css'; import styles from './MovieHistoryRow.css';
@ -20,7 +21,8 @@ function getTitle(eventType) {
case 'downloadFolderImported': return 'Download Folder Imported'; case 'downloadFolderImported': return 'Download Folder Imported';
case 'downloadFailed': return 'Download Failed'; case 'downloadFailed': return 'Download Failed';
case 'episodeFileDeleted': return 'Episode File Deleted'; 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'; default: return 'Unknown';
} }
} }
@ -62,10 +64,10 @@ class MovieHistoryRow extends Component {
eventType, eventType,
sourceTitle, sourceTitle,
quality, quality,
languages,
qualityCutoffNotMet, qualityCutoffNotMet,
date, date,
data data
// movie,
} = this.props; } = this.props;
const { const {
@ -83,6 +85,12 @@ class MovieHistoryRow extends Component {
{sourceTitle} {sourceTitle}
</TableRowCell> </TableRowCell>
<TableRowCell>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
<TableRowCell> <TableRowCell>
<MovieQuality <MovieQuality
quality={quality} quality={quality}
@ -142,13 +150,11 @@ MovieHistoryRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
eventType: PropTypes.string.isRequired, eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
language: PropTypes.object.isRequired, languages: PropTypes.arrayOf(PropTypes.object).isRequired,
languageCutoffNotMet: PropTypes.bool.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired, data: PropTypes.object.isRequired,
fullSeries: PropTypes.bool.isRequired,
movie: PropTypes.object.isRequired, movie: PropTypes.object.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired onMarkAsFailedPress: PropTypes.func.isRequired
}; };

View File

@ -0,0 +1,19 @@
import React from 'react';
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
function MovieHistoryTable(props) {
const {
...otherProps
} = props;
return (
<MovieHistoryTableContentConnector
{...otherProps}
/>
);
}
MovieHistoryTable.propTypes = {
};
export default MovieHistoryTable;

View File

@ -0,0 +1,5 @@
.blankpad {
padding-left:2em;
padding-top: 10px;
padding-bottom: 10px;
}

View File

@ -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 (
<div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>Unable to load history</div>
}
{
isPopulated && !hasItems && !error &&
<div className={styles.blankpad}>No history</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.reverse().map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</div>
);
}
}
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;

View File

@ -2,14 +2,14 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchMovieHistory, clearMovieHistory, seriesHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions'; import { fetchMovieHistory, clearMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryModalContent from './MovieHistoryModalContent'; import MovieHistoryTableContent from './MovieHistoryTableContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.moviesHistory, (state) => state.movieHistory,
(seriesHistory) => { (movieHistory) => {
return seriesHistory; return movieHistory;
} }
); );
} }
@ -17,23 +17,21 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
fetchMovieHistory, fetchMovieHistory,
clearMovieHistory, clearMovieHistory,
seriesHistoryMarkAsFailed movieHistoryMarkAsFailed
}; };
class MovieHistoryModalContentConnector extends Component { class MovieHistoryTableContentConnector extends Component {
// //
// Lifecycle // Lifecycle
componentDidMount() { componentDidMount() {
const { const {
seriesId, movieId
seasonNumber
} = this.props; } = this.props;
this.props.fetchMovieHistory({ this.props.fetchMovieHistory({
seriesId, movieId
seasonNumber
}); });
} }
@ -46,14 +44,12 @@ class MovieHistoryModalContentConnector extends Component {
onMarkAsFailedPress = (historyId) => { onMarkAsFailedPress = (historyId) => {
const { const {
seriesId, movieId
seasonNumber
} = this.props; } = this.props;
this.props.seriesHistoryMarkAsFailed({ this.props.movieHistoryMarkAsFailed({
historyId, historyId,
seriesId, movieId
seasonNumber
}); });
} }
@ -62,7 +58,7 @@ class MovieHistoryModalContentConnector extends Component {
render() { render() {
return ( return (
<MovieHistoryModalContent <MovieHistoryTableContent
{...this.props} {...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress} onMarkAsFailedPress={this.onMarkAsFailedPress}
/> />
@ -70,12 +66,11 @@ class MovieHistoryModalContentConnector extends Component {
} }
} }
MovieHistoryModalContentConnector.propTypes = { MovieHistoryTableContentConnector.propTypes = {
seriesId: PropTypes.number.isRequired, movieId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number,
fetchMovieHistory: PropTypes.func.isRequired, fetchMovieHistory: PropTypes.func.isRequired,
clearMovieHistory: 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);

View File

@ -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 (
<Label
className={className}
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
>
{languages[0].name}
</Label>
);
}
return (
<Popover
className={className}
anchor={
<Label
className={className}
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
>
Multi-Language
</Label>
}
title="Languages"
body={
<ul>
{
languages.map((language) => {
return (
<li key={language.id}>
{language.name}
</li>
);
})
}
</ul>
}
position={tooltipPositions.LEFT}
/>
);
}
MovieLanguage.propTypes = {
className: PropTypes.string,
languages: PropTypes.arrayOf(PropTypes.object),
isCutoffNotMet: PropTypes.bool
};
MovieLanguage.defaultProps = {
isCutoffNotMet: true
};
export default MovieLanguage;

View File

@ -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 (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<SeasonInteractiveSearchModalContent
seriesId={seriesId}
seasonNumber={seasonNumber}
onModalClose={onModalClose}
/>
</Modal>
);
}
SeasonInteractiveSearchModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
seriesId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SeasonInteractiveSearchModal;

View File

@ -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);

View File

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Interactive Search
</ModalHeader>
<ModalBody>
<InteractiveSearchConnector
type="season"
searchPayload={{
seriesId,
seasonNumber
}}
/>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
);
}
SeasonInteractiveSearchModalContent.propTypes = {
seriesId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SeasonInteractiveSearchModalContent;

View File

@ -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 (
<TableRow>
<TableRowCell>
{title}
</TableRowCell>
<TableRowCell>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
</TableRow>
);
}
}
MovieTitlesRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
language: PropTypes.object.isRequired
};
export default MovieTitlesRow;

View File

@ -0,0 +1,19 @@
import React from 'react';
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
function MovieTitlesTable(props) {
const {
...otherProps
} = props;
return (
<MovieTitlesTableContentConnector
{...otherProps}
/>
);
}
MovieTitlesTable.propTypes = {
};
export default MovieTitlesTable;

View File

@ -0,0 +1,5 @@
.blankpad {
padding-left:2em;
padding-top: 10px;
padding-bottom: 10px;
}

View File

@ -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 (
<div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>Unable to load alternative titles.</div>
}
{
isPopulated && !hasItems && !error &&
<div className={styles.blankpad}>No alternative titles.</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.reverse().map((item) => {
return (
<MovieTitlesRow
key={item.id}
{...item}
/>
);
})
}
</TableBody>
</Table>
}
</div>
);
}
}
MovieTitlesTableContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default MovieTitlesTableContent;

View File

@ -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 (
<MovieTitlesTableContent
{...this.props}
items={movie[0].alternateTitles}
/>
);
}
}
MovieTitlesTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);

View File

@ -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 (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
{
isOpen &&
<MovieFileEditorModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
}
</Modal>
);
}
MovieFileEditorModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieFileEditorModal;

View File

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manage Episodes
</ModalHeader>
<ModalBody>
{
!items.length &&
<div>
No episode files to manage.
</div>
}
{
!!items.length &&
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<MovieFileEditorRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
}
</ModalBody>
<ModalFooter>
<div className={styles.actions}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!hasSelectedFiles}
onPress={this.onDeletePress}
>
Delete
</SpinnerButton>
<div className={styles.selectInput}>
<SelectInput
name="language"
value="selectLanguage"
values={languageOptions}
isDisabled={!hasSelectedFiles}
onChange={this.onLanguageChange}
/>
</div>
<div className={styles.selectInput}>
<SelectInput
name="quality"
value="selectQuality"
values={qualityOptions}
isDisabled={!hasSelectedFiles}
onChange={this.onQualityChange}
/>
</div>
</div>
<Button
onPress={onModalClose}
>
Close
</Button>
</ModalFooter>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title="Delete Selected Episode Files"
message={'Are you sure you want to delete the selected episode files?'}
confirmLabel="Delete"
onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose}
/>
</ModalContent>
);
}
}
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;

View File

@ -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;
}

View File

@ -1,62 +1,195 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React, { Component } from 'react';
import Label from 'Components/Label'; import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import { icons, kinds } from 'Helpers/Props';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; 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 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) { class MovieFileEditorRow extends Component {
const {
id,
relativePath,
airDateUtc,
language,
quality,
isSelected,
onSelectedChange
} = props;
return ( //
<TableRow> // Lifecycle
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell> constructor(props, context) {
{relativePath} super(props, context);
</TableRowCell>
<RelativeDateCellConnector this.state = {
date={airDateUtc} isSelectQualityModalOpen: false,
/> isSelectLanguageModalOpen: false,
isConfirmDeleteModalOpen: false
};
}
<TableRowCell> //
<Label> // Listeners
{language.name}
</Label>
</TableRowCell>
<TableRowCell> onSelectQualityPress = () => {
<MovieQuality this.setState({ isSelectQualityModalOpen: true });
quality={quality} }
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 (
<TableRow>
<TableRowCell
className={styles.relativePath}
title={relativePath}
>
{relativePath}
</TableRowCell>
<TableRowCell>
<MediaInfoConnector
movieFileId={id}
type={mediaInfoTypes.VIDEO}
/>
<MediaInfoConnector
movieFileId={id}
type={mediaInfoTypes.AUDIO}
/>
</TableRowCell>
<TableRowCellButton
className={styles.language}
title="Click to change language"
onPress={this.onSelectLanguagePress}
>
{
showLanguagePlaceholder &&
<MovieFileRowCellPlaceholder />
}
{
!showLanguagePlaceholder && !!languages &&
<MovieLanguage
className={styles.label}
languages={languages}
/>
}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
title="Click to change quality"
onPress={this.onSelectQualityPress}
>
{
showQualityPlaceholder &&
<MovieFileRowCellPlaceholder />
}
{
!showQualityPlaceholder && !!quality &&
<MovieQuality
className={styles.label}
quality={quality}
/>
}
</TableRowCellButton>
<TableRowCell className={styles.actions}>
<IconButton
title="Delete file"
name={icons.REMOVE}
onPress={this.onDeletePress}
/>
</TableRowCell>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
ids={[id]}
kind={kinds.DANGER}
title="Delete Selected Movie Files"
message={'Are you sure you want to delete the selected movie files?'}
confirmLabel="Delete"
onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose}
/> />
</TableRowCell>
</TableRow> <SelectQualityModal
); isOpen={isSelectQualityModalOpen}
ids={[id]}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose}
/>
<SelectLanguageModal
isOpen={isSelectLanguageModalOpen}
ids={[id]}
languageId={languages[0] ? languages[0].id : 0}
onModalClose={this.onSelectLanguageModalClose}
/>
</TableRow>
);
}
} }
MovieFileEditorRow.propTypes = { MovieFileEditorRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
relativePath: PropTypes.string.isRequired, relativePath: PropTypes.string.isRequired,
airDateUtc: PropTypes.string.isRequired,
language: PropTypes.object.isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
isSelected: PropTypes.bool, languages: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired mediaInfo: PropTypes.object.isRequired,
onDeletePress: PropTypes.func.isRequired
}; };
export default MovieFileEditorRow; export default MovieFileEditorRow;

View File

@ -0,0 +1,21 @@
import PropTypes from 'prop-types';
import React from 'react';
import MovieFileEditorTableContentConnector from './MovieFileEditorTableContentConnector';
function MovieFileEditorTable(props) {
const {
movieId
} = props;
return (
<MovieFileEditorTableContentConnector
movieId={movieId}
/>
);
}
MovieFileEditorTable.propTypes = {
movieId: PropTypes.number.isRequired
};
export default MovieFileEditorTable;

View File

@ -6,3 +6,9 @@
.selectInput { .selectInput {
margin-left: 10px; margin-left: 10px;
} }
.blankpad {
padding-left:2em;
padding-top: 10px;
padding-bottom: 10px;
}

View File

@ -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 (
<div>
{
!items.length &&
<div className={styles.blankpad}>
No movie files to manage.
</div>
}
{
!!items.length &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieFileEditorRow
key={item.id}
{...item}
onDeletePress={this.props.onDeletePress}
/>
);
})
}
</TableBody>
</Table>
}
</div>
);
}
}
MovieFileEditorTableContent.propTypes = {
movieId: PropTypes.number,
isDeleting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired
};
export default MovieFileEditorTableContent;

View File

@ -6,26 +6,30 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import getQualities from 'Utilities/Quality/getQualities'; import getQualities from 'Utilities/Quality/getQualities';
import createMovieSelector from 'Store/Selectors/createMovieSelector'; import createMovieSelector from 'Store/Selectors/createMovieSelector';
import { deleteMovieFiles, updateMovieFiles } from 'Store/Actions/movieFileActions'; import { deleteMovieFile, updateMovieFiles } from 'Store/Actions/movieFileActions';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; import { fetchQualityProfileSchema, fetchLanguages } from 'Store/Actions/settingsActions';
import MovieFileEditorModalContent from './MovieFileEditorModalContent'; import MovieFileEditorTableContent from './MovieFileEditorTableContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.movieFiles, (state) => state.movieFiles,
(state) => state.settings.qualityProfiles.schema, (state) => state.settings.languages,
(state) => state.settings.qualityProfiles,
createMovieSelector(), createMovieSelector(),
( (
movieFiles, movieFiles,
qualityProfileSchema, languageProfiles,
movie qualityProfiles
) => { ) => {
const qualities = getQualities(qualityProfileSchema.items); const languages = languageProfiles.items;
const qualities = getQualities(qualityProfiles.schema.items);
return { return {
items: movieFiles.items, items: movieFiles.items,
isDeleting: movieFiles.isDeleting, isDeleting: movieFiles.isDeleting,
isSaving: movieFiles.isSaving, isSaving: movieFiles.isSaving,
error: null,
languages,
qualities qualities
}; };
} }
@ -38,22 +42,27 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(fetchQualityProfileSchema()); dispatch(fetchQualityProfileSchema());
}, },
dispatchFetchLanguages(name, path) {
dispatch(fetchLanguages());
},
dispatchUpdateMovieFiles(updateProps) { dispatchUpdateMovieFiles(updateProps) {
dispatch(updateMovieFiles(updateProps)); dispatch(updateMovieFiles(updateProps));
}, },
onDeletePress(episodeFileIds) { onDeletePress(movieFileId) {
dispatch(deleteMovieFiles({ episodeFileIds })); dispatch(deleteMovieFile(movieFileId));
} }
}; };
} }
class MovieFileEditorModalContentConnector extends Component { class MovieFileEditorTableContentConnector extends Component {
// //
// Lifecycle // Lifecycle
componentDidMount() { componentDidMount() {
this.props.dispatchFetchLanguages();
this.props.dispatchFetchQualityProfileSchema(); this.props.dispatchFetchQualityProfileSchema();
} }
@ -63,7 +72,14 @@ class MovieFileEditorModalContentConnector extends Component {
// //
// Listeners // 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 = { const quality = {
quality: _.find(this.props.qualities, { id: qualityId }), quality: _.find(this.props.qualities, { id: qualityId }),
revision: { revision: {
@ -72,31 +88,34 @@ class MovieFileEditorModalContentConnector extends Component {
} }
}; };
this.props.dispatchUpdateMovieFiles({ episodeFileIds, quality }); this.props.dispatchUpdateMovieFiles({ movieFileIds, quality });
} }
render() { render() {
const { const {
dispatchFetchLanguages,
dispatchFetchQualityProfileSchema, dispatchFetchQualityProfileSchema,
dispatchUpdateMovieFiles, dispatchUpdateMovieFiles,
...otherProps ...otherProps
} = this.props; } = this.props;
return ( return (
<MovieFileEditorModalContent <MovieFileEditorTableContent
{...otherProps} {...otherProps}
onLanguageChange={this.onLanguageChange}
onQualityChange={this.onQualityChange} onQualityChange={this.onQualityChange}
/> />
); );
} }
} }
MovieFileEditorModalContentConnector.propTypes = { MovieFileEditorTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired, movieId: PropTypes.number.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired, languages: PropTypes.arrayOf(PropTypes.object).isRequired,
qualities: PropTypes.arrayOf(PropTypes.object).isRequired, qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
dispatchUpdateMovieFiles: PropTypes.func.isRequired dispatchUpdateMovieFiles: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieFileEditorModalContentConnector); export default connect(createMapStateToProps, createMapDispatchToProps)(MovieFileEditorTableContentConnector);

View File

@ -0,0 +1,7 @@
.placeholder {
display: inline-block;
margin: -8px 0;
width: 100%;
height: 25px;
border: 2px dashed $dangerColor;
}

View File

@ -0,0 +1,10 @@
import React from 'react';
import styles from './MovieFileRowCellPlaceholder.css';
function MovieFileRowCellPlaceholder() {
return (
<span className={styles.placeholder} />
);
}
export default MovieFileRowCellPlaceholder;

View File

@ -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 (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectLanguageModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectLanguageModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectLanguageModal;

View File

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Language
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load languages</div>
}
{
isPopulated && !error &&
<Form>
<FormGroup>
<FormLabel>Language</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="language"
value={languageId}
values={languageOptions}
onChange={onLanguageSelect}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
</ModalFooter>
</ModalContent>
);
}
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;

View File

@ -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 (
<SelectLanguageModalContent
{...this.props}
onLanguageSelect={this.onLanguageSelect}
/>
);
}
}
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);

View File

@ -6,10 +6,10 @@ import MediaInfo from './MediaInfo';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createMovieFileSelector(), createMovieFileSelector(),
(episodeFile) => { (movieFile) => {
if (episodeFile) { if (movieFile) {
return { return {
...episodeFile.mediaInfo ...movieFile.mediaInfo
}; };
} }

View File

@ -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 (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectQualityModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectQualityModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectQualityModal;

View File

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Quality
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load qualities</div>
}
{
isPopulated && !error &&
<Form>
<FormGroup>
<FormLabel>Quality</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="quality"
value={qualityId}
values={qualityOptions}
onChange={this.onQualityChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Proper</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="proper"
value={proper}
onChange={this.onProperChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Real</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="real"
value={real}
onChange={this.onRealChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onQualitySelect}
>
Select Quality
</Button>
</ModalFooter>
</ModalContent>
);
}
}
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;

View File

@ -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 (
<SelectQualityModalContent
{...this.props}
onQualitySelect={this.onQualitySelect}
/>
);
}
}
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);

View File

@ -38,9 +38,16 @@ export const defaultState = {
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'language',
label: 'Language',
isSortable: true,
isVisible: true
},
{ {
name: 'quality', name: 'quality',
label: 'Quality', label: 'Quality',
isSortable: true,
isVisible: true isVisible: true
}, },
{ {

View File

@ -42,11 +42,13 @@ export const defaultState = {
{ {
name: 'language', name: 'language',
label: 'Language', label: 'Language',
isVisible: false isSortable: true,
isVisible: true
}, },
{ {
name: 'quality', name: 'quality',
label: 'Quality', label: 'Quality',
isSortable: true,
isVisible: true isVisible: true
}, },
{ {

View File

@ -137,9 +137,10 @@ export const actionHandlers = handleThunks({
}, },
[UPDATE_MOVIE_FILES]: function(getState, payload, dispatch) { [UPDATE_MOVIE_FILES]: function(getState, payload, dispatch) {
const { const {
movieFileIds, movieFileIds,
language, languages,
quality quality
} = payload; } = payload;
@ -149,8 +150,8 @@ export const actionHandlers = handleThunks({
movieFileIds movieFileIds
}; };
if (language) { if (languages) {
data.language = language; data.languages = languages;
} }
if (quality) { if (quality) {
@ -169,8 +170,8 @@ export const actionHandlers = handleThunks({
...movieFileIds.map((id) => { ...movieFileIds.map((id) => {
const props = {}; const props = {};
if (language) { if (languages) {
props.language = language; props.languages = languages;
} }
if (quality) { if (quality) {

View File

@ -23,27 +23,27 @@ export const defaultState = {
// //
// Actions Types // Actions Types
export const FETCH_SERIES_HISTORY = 'seriesHistory/fetchMovieHistory'; export const FETCH_MOVIE_HISTORY = 'movieHistory/fetchMovieHistory';
export const CLEAR_SERIES_HISTORY = 'seriesHistory/clearMovieHistory'; export const CLEAR_MOVIE_HISTORY = 'movieHistory/clearMovieHistory';
export const SERIES_HISTORY_MARK_AS_FAILED = 'seriesHistory/seriesHistoryMarkAsFailed'; export const MOVIE_HISTORY_MARK_AS_FAILED = 'movieHistory/movieHistoryMarkAsFailed';
// //
// Action Creators // Action Creators
export const fetchMovieHistory = createThunk(FETCH_SERIES_HISTORY); export const fetchMovieHistory = createThunk(FETCH_MOVIE_HISTORY);
export const clearMovieHistory = createAction(CLEAR_SERIES_HISTORY); export const clearMovieHistory = createAction(CLEAR_MOVIE_HISTORY);
export const seriesHistoryMarkAsFailed = createThunk(SERIES_HISTORY_MARK_AS_FAILED); export const movieHistoryMarkAsFailed = createThunk(MOVIE_HISTORY_MARK_AS_FAILED);
// //
// Action Handlers // Action Handlers
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[FETCH_SERIES_HISTORY]: function(getState, payload, dispatch) { [FETCH_MOVIE_HISTORY]: function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true })); dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: '/history/series', url: '/history/movie',
data: payload data: payload
}).request; }).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 { const {
historyId, historyId,
seriesId, movieId
seasonNumber
} = payload; } = payload;
const promise = createAjaxRequest({ const promise = createAjaxRequest({
@ -86,7 +85,7 @@ export const actionHandlers = handleThunks({
}).request; }).request;
promise.done(() => { promise.done(() => {
dispatch(fetchMovieHistory({ seriesId, seasonNumber })); dispatch(fetchMovieHistory({ movieId }));
}); });
} }
}); });
@ -96,7 +95,7 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({ export const reducers = createHandleActions({
[CLEAR_SERIES_HISTORY]: (state) => { [CLEAR_MOVIE_HISTORY]: (state) => {
return Object.assign({}, state, defaultState); return Object.assign({}, state, defaultState);
} }

View File

@ -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);

View File

@ -68,6 +68,12 @@ export const defaultState = {
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'languages',
label: 'Languages',
isSortable: true,
isVisible: true
},
{ {
name: 'quality', name: 'quality',
label: 'Quality', label: 'Quality',

View File

@ -11,8 +11,6 @@ import createHandleActions from './Creators/createHandleActions';
// Variables // Variables
export const section = 'releases'; export const section = 'releases';
export const episodeSection = 'releases.episode';
export const seasonSection = 'releases.season';
let abortCurrentRequest = null; let abortCurrentRequest = null;
@ -54,28 +52,6 @@ export const defaultState = {
key: 'all', key: 'all',
label: 'All', label: 'All',
filters: [] 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 type: filterBuilderTypes.NUMBER
} }
], ],
selectedFilterKey: 'all'
episode: {
selectedFilterKey: 'all'
},
season: {
selectedFilterKey: 'season-pack'
}
}; };
export const persistState = [ export const persistState = [
'releases.selectedFilterKey', 'releases.customFilters',
'releases.episode.customFilters', 'releases.selectedFilterKey'
'releases.season.customFilters'
]; ];
// //
@ -171,8 +140,7 @@ export const SET_RELEASES_SORT = 'releases/setReleasesSort';
export const CLEAR_RELEASES = 'releases/clearReleases'; export const CLEAR_RELEASES = 'releases/clearReleases';
export const GRAB_RELEASE = 'releases/grabRelease'; export const GRAB_RELEASE = 'releases/grabRelease';
export const UPDATE_RELEASE = 'releases/updateRelease'; export const UPDATE_RELEASE = 'releases/updateRelease';
export const SET_EPISODE_RELEASES_FILTER = 'releases/setEpisodeReleasesFilter'; export const SET_RELEASES_FILTER = 'releases/setMovieReleasesFilter';
export const SET_SEASON_RELEASES_FILTER = 'releases/setSeasonReleasesFilter';
// //
// Action Creators // Action Creators
@ -183,8 +151,7 @@ export const setReleasesSort = createAction(SET_RELEASES_SORT);
export const clearReleases = createAction(CLEAR_RELEASES); export const clearReleases = createAction(CLEAR_RELEASES);
export const grabRelease = createThunk(GRAB_RELEASE); export const grabRelease = createThunk(GRAB_RELEASE);
export const updateRelease = createAction(UPDATE_RELEASE); export const updateRelease = createAction(UPDATE_RELEASE);
export const setEpisodeReleasesFilter = createAction(SET_EPISODE_RELEASES_FILTER); export const setReleasesFilter = createAction(SET_RELEASES_FILTER);
export const setSeasonReleasesFilter = createAction(SET_SEASON_RELEASES_FILTER);
// //
// Helpers // Helpers
@ -248,13 +215,7 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({ export const reducers = createHandleActions({
[CLEAR_RELEASES]: (state) => { [CLEAR_RELEASES]: (state) => {
const { return Object.assign({}, state, defaultState);
episode,
season,
...otherDefaultState
} = defaultState;
return Object.assign({}, state, otherDefaultState);
}, },
[UPDATE_RELEASE]: (state, { payload }) => { [UPDATE_RELEASE]: (state, { payload }) => {
@ -276,8 +237,7 @@ export const reducers = createHandleActions({
return newState; return newState;
}, },
[SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section), [SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section),
[SET_EPISODE_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(episodeSection), [SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section)
[SET_SEASON_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(seasonSection)
}, defaultState, section); }, defaultState, section);

View File

@ -148,7 +148,7 @@ private IEnumerable<DownloadDecision> GetMovieDecisions(List<ReleaseInfo> report
} }
else else
{ {
//remoteMovie.DownloadAllowed = true; remoteMovie.DownloadAllowed = true;
decision = GetDecisionForReport(remoteMovie, searchCriteria); decision = GetDecisionForReport(remoteMovie, searchCriteria);
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
namespace Radarr.Api.V2.MovieFiles namespace Radarr.Api.V2.MovieFiles
@ -6,6 +7,7 @@ namespace Radarr.Api.V2.MovieFiles
public class MovieFileListResource public class MovieFileListResource
{ {
public List<int> MovieFileIds { get; set; } public List<int> MovieFileIds { get; set; }
public List<Language> Languages { get; set; }
public QualityModel Quality { get; set; } public QualityModel Quality { get; set; }
} }
} }

View File

@ -42,10 +42,10 @@ public MovieFileModule(IBroadcastSignalRMessage signalRBroadcaster,
GetResourceById = GetMovieFile; GetResourceById = GetMovieFile;
GetResourceAll = GetMovieFiles; GetResourceAll = GetMovieFiles;
UpdateResource = SetQuality; UpdateResource = SetMovieFile;
DeleteResource = DeleteMovieFile; DeleteResource = DeleteMovieFile;
Put["/editor"] = movieFiles => SetQuality(); Put["/editor"] = movieFiles => SetMovieFile();
Delete["/bulk"] = movieFiles => DeleteMovieFiles(); Delete["/bulk"] = movieFiles => DeleteMovieFiles();
} }
@ -92,14 +92,15 @@ private List<MovieFileResource> GetMovieFiles()
} }
} }
private void SetQuality(MovieFileResource movieFileResource) private void SetMovieFile(MovieFileResource movieFileResource)
{ {
var movieFile = _mediaFileService.GetMovie(movieFileResource.Id); var movieFile = _mediaFileService.GetMovie(movieFileResource.Id);
movieFile.Quality = movieFileResource.Quality; movieFile.Quality = movieFileResource.Quality;
movieFile.Languages = movieFileResource.Languages;
_mediaFileService.Update(movieFile); _mediaFileService.Update(movieFile);
} }
private Response SetQuality() private Response SetMovieFile()
{ {
var resource = Request.Body.FromJson<MovieFileListResource>(); var resource = Request.Body.FromJson<MovieFileListResource>();
var movieFiles = _mediaFileService.GetMovies(resource.MovieFileIds); var movieFiles = _mediaFileService.GetMovies(resource.MovieFileIds);
@ -111,6 +112,11 @@ private Response SetQuality()
{ {
movieFile.Quality = resource.Quality; movieFile.Quality = resource.Quality;
} }
if (resource.Languages != null)
{
movieFile.Languages = resource.Languages;
}
} }
_mediaFileService.Update(movieFiles); _mediaFileService.Update(movieFiles);