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:
parent
06b1c03053
commit
12fba024f0
@ -6,6 +6,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||
import styles from './BlacklistRow.css';
|
||||
@ -42,6 +43,7 @@ class BlacklistRow extends Component {
|
||||
movie,
|
||||
sourceTitle,
|
||||
quality,
|
||||
languages,
|
||||
date,
|
||||
protocol,
|
||||
indexer,
|
||||
@ -82,6 +84,16 @@ class BlacklistRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'language') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@ -159,6 +171,7 @@ BlacklistRow.propTypes = {
|
||||
movie: PropTypes.object.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
|
@ -6,6 +6,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
@ -52,6 +53,7 @@ class HistoryRow extends Component {
|
||||
const {
|
||||
movie,
|
||||
quality,
|
||||
languages,
|
||||
qualityCutoffNotMet,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
@ -102,6 +104,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'language') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
@ -193,8 +205,7 @@ class HistoryRow extends Component {
|
||||
HistoryRow.propTypes = {
|
||||
movieId: PropTypes.number,
|
||||
movie: PropTypes.object.isRequired,
|
||||
language: PropTypes.object.isRequired,
|
||||
languageCutoffNotMet: PropTypes.bool.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
|
@ -10,6 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
@ -69,6 +70,7 @@ class QueueRow extends Component {
|
||||
errorMessage,
|
||||
movie,
|
||||
quality,
|
||||
languages,
|
||||
protocol,
|
||||
indexer,
|
||||
outputPath,
|
||||
@ -145,6 +147,16 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'language') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
@ -297,6 +309,7 @@ QueueRow.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
movie: PropTypes.object.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
|
@ -128,7 +128,7 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
folderName: item.folderName,
|
||||
movieId: movie.id,
|
||||
quality,
|
||||
language,
|
||||
languages: [language],
|
||||
downloadId: this.props.downloadId
|
||||
});
|
||||
}
|
||||
|
@ -7,8 +7,6 @@
|
||||
.quality,
|
||||
.language {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
|
@ -9,7 +9,7 @@ import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
// import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
@ -152,7 +152,8 @@ class InteractiveImportRow extends Component {
|
||||
const showMoviePlaceholder = isSelected && !movie;
|
||||
const showQualityPlaceholder = isSelected && !quality;
|
||||
const showLanguagePlaceholder = isSelected && !language;
|
||||
|
||||
// TODO - Placeholder till we implement selection of multiple languages
|
||||
const languages = [language];
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
@ -207,13 +208,13 @@ class InteractiveImportRow extends Component {
|
||||
<InteractiveImportRowCellPlaceholder />
|
||||
}
|
||||
|
||||
{/* {
|
||||
{
|
||||
!showLanguagePlaceholder && !!language &&
|
||||
<MovieLanguage
|
||||
className={styles.label}
|
||||
language={language}
|
||||
languages={languages}
|
||||
/>
|
||||
} */}
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCell>
|
||||
|
@ -9,7 +9,7 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||
import styles from './InteractiveSearch.css';
|
||||
import styles from './InteractiveSearchContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@ -48,6 +48,12 @@ const columns = [
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languageWeight',
|
||||
label: 'Language',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'qualityWeight',
|
||||
label: 'Quality',
|
||||
@ -70,7 +76,7 @@ const columns = [
|
||||
}
|
||||
];
|
||||
|
||||
function InteractiveSearch(props) {
|
||||
function InteractiveSearchContent(props) {
|
||||
const {
|
||||
searchPayload,
|
||||
isFetching,
|
||||
@ -83,7 +89,6 @@ function InteractiveSearch(props) {
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
type,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onSortPress,
|
||||
@ -101,7 +106,6 @@ function InteractiveSearch(props) {
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||
filterModalConnectorComponentProps={{ type }}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
@ -169,7 +173,7 @@ function InteractiveSearch(props) {
|
||||
);
|
||||
}
|
||||
|
||||
InteractiveSearch.propTypes = {
|
||||
InteractiveSearchContent.propTypes = {
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
@ -181,7 +185,6 @@ InteractiveSearch.propTypes = {
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
@ -189,4 +192,4 @@ InteractiveSearch.propTypes = {
|
||||
onGrabPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveSearch;
|
||||
export default InteractiveSearchContent;
|
@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
|
||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import InteractiveSearch from './InteractiveSearch';
|
||||
import InteractiveSearchContent from './InteractiveSearchContent';
|
||||
|
||||
function createMapStateToProps(appState, { type }) {
|
||||
function createMapStateToProps(appState) {
|
||||
return createSelector(
|
||||
(state) => state.releases.items.length,
|
||||
createClientSideCollectionSelector('releases', `releases.${type}`),
|
||||
createClientSideCollectionSelector('releases'),
|
||||
createUISettingsSelector(),
|
||||
(totalReleasesCount, releases, uiSettings) => {
|
||||
return {
|
||||
@ -29,15 +29,16 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatch(releaseActions.fetchReleases(payload));
|
||||
},
|
||||
|
||||
dispatchClearReleases(payload) {
|
||||
dispatch(releaseActions.clearReleases(payload));
|
||||
},
|
||||
|
||||
onSortPress(sortKey, sortDirection) {
|
||||
dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection }));
|
||||
},
|
||||
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
const action = props.type === 'episode' ?
|
||||
releaseActions.setEpisodeReleasesFilter :
|
||||
releaseActions.setSeasonReleasesFilter;
|
||||
|
||||
const action = releaseActions.setReleasesFilter;
|
||||
dispatch(action({ selectedFilterKey }));
|
||||
},
|
||||
|
||||
@ -47,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
};
|
||||
}
|
||||
|
||||
class InteractiveSearchConnector extends Component {
|
||||
class InteractiveSearchContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@ -61,7 +62,6 @@ class InteractiveSearchConnector extends Component {
|
||||
|
||||
// If search results are not yet isPopulated fetch them,
|
||||
// otherwise re-show the existing props.
|
||||
|
||||
if (!isPopulated) {
|
||||
dispatchFetchReleases(searchPayload);
|
||||
}
|
||||
@ -73,22 +73,24 @@ class InteractiveSearchConnector extends Component {
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchReleases,
|
||||
dispatchClearReleases,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
||||
<InteractiveSearch
|
||||
<InteractiveSearchContent
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveSearchConnector.propTypes = {
|
||||
InteractiveSearchContentConnector.propTypes = {
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
dispatchFetchReleases: PropTypes.func.isRequired
|
||||
dispatchFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchClearReleases: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);
|
@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setEpisodeReleasesFilter, setSeasonReleasesFilter } from 'Store/Actions/releaseActions';
|
||||
import { setReleasesFilter } from 'Store/Actions/releaseActions';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@ -20,10 +20,7 @@ function createMapStateToProps() {
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchSetFilter(payload) {
|
||||
const action = props.type === 'episode' ?
|
||||
setEpisodeReleasesFilter:
|
||||
setSeasonReleasesFilter;
|
||||
|
||||
const action = setReleasesFilter;
|
||||
dispatch(action(payload));
|
||||
}
|
||||
};
|
||||
|
@ -1,13 +1,10 @@
|
||||
.title {
|
||||
.quality,
|
||||
.language {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.quality {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
text-align: center;
|
||||
.language {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.rejected,
|
||||
|
@ -11,10 +11,11 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import Peers from './Peers';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
|
||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
@ -111,6 +112,7 @@ class InteractiveSearchRow extends Component {
|
||||
seeders,
|
||||
leechers,
|
||||
quality,
|
||||
languages,
|
||||
rejections,
|
||||
downloadAllowed,
|
||||
isGrabbing,
|
||||
@ -159,8 +161,14 @@ class InteractiveSearchRow extends Component {
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.language}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.quality}>
|
||||
<EpisodeQuality
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
@ -199,6 +207,7 @@ class InteractiveSearchRow extends Component {
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
||||
/>
|
||||
@ -208,7 +217,7 @@ class InteractiveSearchRow extends Component {
|
||||
isOpen={this.state.isConfirmGrabModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title="Grab Release"
|
||||
message={`Sonarr was unable to determine which series and episode this release was for. Sonarr may be unable to automatically import this release. Do you want to grab '${title}'?`}
|
||||
message={`Radarr was unable to determine which movie this release was for. Radarr may be unable to automatically import this release. Do you want to grab '${title}'?`}
|
||||
confirmLabel="Grab"
|
||||
onConfirm={this.onGrabConfirm}
|
||||
onCancel={this.onGrabCancel}
|
||||
@ -233,6 +242,7 @@ InteractiveSearchRow.propTypes = {
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number,
|
||||
quality: PropTypes.object.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
downloadAllowed: PropTypes.bool.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
|
16
frontend/src/InteractiveSearch/InteractiveSearchTable.js
Normal file
16
frontend/src/InteractiveSearch/InteractiveSearchTable.js
Normal 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;
|
@ -22,15 +22,17 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import MovieFileEditorModal from 'MovieFile/Editor/MovieFileEditorModal';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
|
||||
import MovieTitlesTable from 'Movie/Titles/MovieTitlesTable';
|
||||
import MovieAlternateTitles from './MovieAlternateTitles';
|
||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||
import InteractiveSearchTable from '../../InteractiveSearch/InteractiveSearchTable';
|
||||
// import MovieTagsConnector from './MovieTagsConnector';
|
||||
import styles from './MovieDetails.css';
|
||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||
@ -68,7 +70,6 @@ class MovieDetails extends Component {
|
||||
isManageEpisodesOpen: false,
|
||||
isEditMovieModalOpen: false,
|
||||
isDeleteMovieModalOpen: false,
|
||||
isMovieHistoryModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
@ -123,14 +124,6 @@ class MovieDetails extends Component {
|
||||
this.setState({ isDeleteMovieModalOpen: false });
|
||||
}
|
||||
|
||||
onMovieHistoryPress = () => {
|
||||
this.setState({ isMovieHistoryModalOpen: true });
|
||||
}
|
||||
|
||||
onMovieHistoryModalClose = () => {
|
||||
this.setState({ isMovieHistoryModalOpen: false });
|
||||
}
|
||||
|
||||
onExpandAllPress = () => {
|
||||
const {
|
||||
allExpanded,
|
||||
@ -195,10 +188,8 @@ class MovieDetails extends Component {
|
||||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isManageEpisodesOpen,
|
||||
isEditMovieModalOpen,
|
||||
isDeleteMovieModalOpen,
|
||||
isMovieHistoryModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
overviewHeight
|
||||
} = this.state;
|
||||
@ -488,19 +479,27 @@ class MovieDetails extends Component {
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<h2>Any content 1</h2>
|
||||
<MovieHistoryTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<h2>Any content 2</h2>
|
||||
<InteractiveSearchTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<h2>Any content 3</h2>
|
||||
<MovieFileEditorTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<h2>Any content 4</h2>
|
||||
<MovieTitlesTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@ -512,18 +511,6 @@ class MovieDetails extends Component {
|
||||
onModalClose={this.onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<MovieFileEditorModal
|
||||
isOpen={isManageEpisodesOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onManageEpisodesModalClose}
|
||||
/>
|
||||
|
||||
<MovieHistoryModal
|
||||
isOpen={isMovieHistoryModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onMovieHistoryModalClose}
|
||||
/>
|
||||
|
||||
<EditMovieModalConnector
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={id}
|
||||
|
@ -10,6 +10,7 @@ import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import { fetchMovieFiles, clearMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { clearReleases } from 'Store/Actions/releaseActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import MovieDetails from './MovieDetails';
|
||||
@ -108,6 +109,7 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
fetchMovieFiles,
|
||||
clearMovieFiles,
|
||||
clearReleases,
|
||||
toggleMovieMonitored,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails,
|
||||
@ -169,6 +171,7 @@ class MovieDetailsConnector extends Component {
|
||||
unpopulate = () => {
|
||||
this.props.clearMovieFiles();
|
||||
this.props.clearQueueDetails();
|
||||
this.props.clearReleases();
|
||||
}
|
||||
|
||||
//
|
||||
@ -220,6 +223,7 @@ MovieDetailsConnector.propTypes = {
|
||||
isRenamingMovie: PropTypes.bool.isRequired,
|
||||
fetchMovieFiles: PropTypes.func.isRequired,
|
||||
clearMovieFiles: PropTypes.func.isRequired,
|
||||
clearReleases: PropTypes.func.isRequired,
|
||||
toggleMovieMonitored: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired,
|
||||
|
@ -5,12 +5,12 @@
|
||||
|
||||
.downloaded {
|
||||
padding-left: 2px;
|
||||
border-left: 4px solid $dangerColor;
|
||||
border-left: 4px solid $successColor;
|
||||
}
|
||||
|
||||
.unaired {
|
||||
.unreleased {
|
||||
padding-left: 2px;
|
||||
border-left: 4px solid $gray;
|
||||
border-left: 4px solid $primaryColor;
|
||||
}
|
||||
|
||||
.unmonitored {
|
||||
|
@ -18,7 +18,7 @@ function getMovieStatus(hasFile, isMonitored, inCinemas) {
|
||||
return 'Missing';
|
||||
}
|
||||
|
||||
return 'Unaired';
|
||||
return 'Unreleased';
|
||||
}
|
||||
|
||||
function MovieStatusLabel(props) {
|
||||
|
@ -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;
|
@ -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;
|
@ -9,6 +9,7 @@ import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import styles from './MovieHistoryRow.css';
|
||||
@ -20,7 +21,8 @@ function getTitle(eventType) {
|
||||
case 'downloadFolderImported': return 'Download Folder Imported';
|
||||
case 'downloadFailed': return 'Download Failed';
|
||||
case 'episodeFileDeleted': return 'Episode File Deleted';
|
||||
case 'episodeFileRenamed': return 'Episode File Renamed';
|
||||
case 'movieFileDeleted': return 'Movie File Deleted';
|
||||
case 'movieFolderImported': return 'Movie Folder Imported';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
}
|
||||
@ -62,10 +64,10 @@ class MovieHistoryRow extends Component {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
quality,
|
||||
languages,
|
||||
qualityCutoffNotMet,
|
||||
date,
|
||||
data
|
||||
// movie,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -83,6 +85,12 @@ class MovieHistoryRow extends Component {
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
@ -142,13 +150,11 @@ MovieHistoryRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
language: PropTypes.object.isRequired,
|
||||
languageCutoffNotMet: PropTypes.bool.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
fullSeries: PropTypes.bool.isRequired,
|
||||
movie: PropTypes.object.isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
19
frontend/src/Movie/History/MovieHistoryTable.js
Normal file
19
frontend/src/Movie/History/MovieHistoryTable.js
Normal 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;
|
5
frontend/src/Movie/History/MovieHistoryTableContent.css
Normal file
5
frontend/src/Movie/History/MovieHistoryTableContent.css
Normal file
@ -0,0 +1,5 @@
|
||||
.blankpad {
|
||||
padding-left:2em;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
110
frontend/src/Movie/History/MovieHistoryTableContent.js
Normal file
110
frontend/src/Movie/History/MovieHistoryTableContent.js
Normal 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;
|
@ -2,14 +2,14 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchMovieHistory, clearMovieHistory, seriesHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
|
||||
import MovieHistoryModalContent from './MovieHistoryModalContent';
|
||||
import { fetchMovieHistory, clearMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
|
||||
import MovieHistoryTableContent from './MovieHistoryTableContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.moviesHistory,
|
||||
(seriesHistory) => {
|
||||
return seriesHistory;
|
||||
(state) => state.movieHistory,
|
||||
(movieHistory) => {
|
||||
return movieHistory;
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -17,23 +17,21 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
fetchMovieHistory,
|
||||
clearMovieHistory,
|
||||
seriesHistoryMarkAsFailed
|
||||
movieHistoryMarkAsFailed
|
||||
};
|
||||
|
||||
class MovieHistoryModalContentConnector extends Component {
|
||||
class MovieHistoryTableContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
seriesId,
|
||||
seasonNumber
|
||||
movieId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchMovieHistory({
|
||||
seriesId,
|
||||
seasonNumber
|
||||
movieId
|
||||
});
|
||||
}
|
||||
|
||||
@ -46,14 +44,12 @@ class MovieHistoryModalContentConnector extends Component {
|
||||
|
||||
onMarkAsFailedPress = (historyId) => {
|
||||
const {
|
||||
seriesId,
|
||||
seasonNumber
|
||||
movieId
|
||||
} = this.props;
|
||||
|
||||
this.props.seriesHistoryMarkAsFailed({
|
||||
this.props.movieHistoryMarkAsFailed({
|
||||
historyId,
|
||||
seriesId,
|
||||
seasonNumber
|
||||
movieId
|
||||
});
|
||||
}
|
||||
|
||||
@ -62,7 +58,7 @@ class MovieHistoryModalContentConnector extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MovieHistoryModalContent
|
||||
<MovieHistoryTableContent
|
||||
{...this.props}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
@ -70,12 +66,11 @@ class MovieHistoryModalContentConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryModalContentConnector.propTypes = {
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
seasonNumber: PropTypes.number,
|
||||
MovieHistoryTableContentConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
fetchMovieHistory: PropTypes.func.isRequired,
|
||||
clearMovieHistory: PropTypes.func.isRequired,
|
||||
seriesHistoryMarkAsFailed: PropTypes.func.isRequired
|
||||
movieHistoryMarkAsFailed: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryTableContentConnector);
|
69
frontend/src/Movie/MovieLanguage.js
Normal file
69
frontend/src/Movie/MovieLanguage.js
Normal 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;
|
@ -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;
|
@ -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);
|
@ -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;
|
45
frontend/src/Movie/Titles/MovieTitlesRow.js
Normal file
45
frontend/src/Movie/Titles/MovieTitlesRow.js
Normal 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;
|
19
frontend/src/Movie/Titles/MovieTitlesTable.js
Normal file
19
frontend/src/Movie/Titles/MovieTitlesTable.js
Normal 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;
|
5
frontend/src/Movie/Titles/MovieTitlesTableContent.css
Normal file
5
frontend/src/Movie/Titles/MovieTitlesTableContent.css
Normal file
@ -0,0 +1,5 @@
|
||||
.blankpad {
|
||||
padding-left:2em;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
81
frontend/src/Movie/Titles/MovieTitlesTableContent.js
Normal file
81
frontend/src/Movie/Titles/MovieTitlesTableContent.js
Normal 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;
|
@ -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);
|
@ -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;
|
@ -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;
|
28
frontend/src/MovieFile/Editor/MovieFileEditorRow.css
Normal file
28
frontend/src/MovieFile/Editor/MovieFileEditorRow.css
Normal 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;
|
||||
}
|
@ -1,62 +1,195 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import SelectQualityModal from 'MovieFile/Quality/SelectQualityModal';
|
||||
import SelectLanguageModal from 'MovieFile/Language/SelectLanguageModal';
|
||||
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
|
||||
import MediaInfoConnector from 'MovieFile/MediaInfoConnector';
|
||||
import MovieFileRowCellPlaceholder from './MovieFileRowCellPlaceholder';
|
||||
import styles from './MovieFileEditorRow.css';
|
||||
|
||||
function MovieFileEditorRow(props) {
|
||||
const {
|
||||
id,
|
||||
relativePath,
|
||||
airDateUtc,
|
||||
language,
|
||||
quality,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = props;
|
||||
class MovieFileEditorRow extends Component {
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
<TableRowCell>
|
||||
{relativePath}
|
||||
</TableRowCell>
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={airDateUtc}
|
||||
/>
|
||||
this.state = {
|
||||
isSelectQualityModalOpen: false,
|
||||
isSelectLanguageModalOpen: false,
|
||||
isConfirmDeleteModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
<TableRowCell>
|
||||
<Label>
|
||||
{language.name}
|
||||
</Label>
|
||||
</TableRowCell>
|
||||
//
|
||||
// Listeners
|
||||
|
||||
<TableRowCell>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
onSelectQualityPress = () => {
|
||||
this.setState({ isSelectQualityModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectLanguagePress = () => {
|
||||
this.setState({ isSelectLanguageModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectQualityModalClose = () => {
|
||||
this.setState({ isSelectQualityModalOpen: false });
|
||||
}
|
||||
|
||||
onSelectLanguageModalClose = () => {
|
||||
this.setState({ isSelectLanguageModalOpen: false });
|
||||
}
|
||||
|
||||
onDeletePress = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmDelete = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
|
||||
this.props.onDeletePress(this.props.id);
|
||||
}
|
||||
|
||||
onConfirmDeleteModalClose = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
relativePath,
|
||||
quality,
|
||||
languages
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isSelectQualityModalOpen,
|
||||
isSelectLanguageModalOpen,
|
||||
isConfirmDeleteModalOpen
|
||||
} = this.state;
|
||||
|
||||
const showQualityPlaceholder = !quality;
|
||||
|
||||
const showLanguagePlaceholder = !languages;
|
||||
|
||||
return (
|
||||
<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 = {
|
||||
id: PropTypes.number.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
airDateUtc: PropTypes.string.isRequired,
|
||||
language: PropTypes.object.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
mediaInfo: PropTypes.object.isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieFileEditorRow;
|
||||
|
21
frontend/src/MovieFile/Editor/MovieFileEditorTable.js
Normal file
21
frontend/src/MovieFile/Editor/MovieFileEditorTable.js
Normal 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;
|
@ -6,3 +6,9 @@
|
||||
.selectInput {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.blankpad {
|
||||
padding-left:2em;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
86
frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js
Normal file
86
frontend/src/MovieFile/Editor/MovieFileEditorTableContent.js
Normal 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;
|
@ -6,26 +6,30 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import getQualities from 'Utilities/Quality/getQualities';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import { deleteMovieFiles, updateMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import MovieFileEditorModalContent from './MovieFileEditorModalContent';
|
||||
import { deleteMovieFile, updateMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { fetchQualityProfileSchema, fetchLanguages } from 'Store/Actions/settingsActions';
|
||||
import MovieFileEditorTableContent from './MovieFileEditorTableContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieFiles,
|
||||
(state) => state.settings.qualityProfiles.schema,
|
||||
(state) => state.settings.languages,
|
||||
(state) => state.settings.qualityProfiles,
|
||||
createMovieSelector(),
|
||||
(
|
||||
movieFiles,
|
||||
qualityProfileSchema,
|
||||
movie
|
||||
languageProfiles,
|
||||
qualityProfiles
|
||||
) => {
|
||||
const qualities = getQualities(qualityProfileSchema.items);
|
||||
const languages = languageProfiles.items;
|
||||
const qualities = getQualities(qualityProfiles.schema.items);
|
||||
|
||||
return {
|
||||
items: movieFiles.items,
|
||||
isDeleting: movieFiles.isDeleting,
|
||||
isSaving: movieFiles.isSaving,
|
||||
error: null,
|
||||
languages,
|
||||
qualities
|
||||
};
|
||||
}
|
||||
@ -38,22 +42,27 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatch(fetchQualityProfileSchema());
|
||||
},
|
||||
|
||||
dispatchFetchLanguages(name, path) {
|
||||
dispatch(fetchLanguages());
|
||||
},
|
||||
|
||||
dispatchUpdateMovieFiles(updateProps) {
|
||||
dispatch(updateMovieFiles(updateProps));
|
||||
},
|
||||
|
||||
onDeletePress(episodeFileIds) {
|
||||
dispatch(deleteMovieFiles({ episodeFileIds }));
|
||||
onDeletePress(movieFileId) {
|
||||
dispatch(deleteMovieFile(movieFileId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class MovieFileEditorModalContentConnector extends Component {
|
||||
class MovieFileEditorTableContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchLanguages();
|
||||
this.props.dispatchFetchQualityProfileSchema();
|
||||
}
|
||||
|
||||
@ -63,7 +72,14 @@ class MovieFileEditorModalContentConnector extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQualityChange = (episodeFileIds, qualityId) => {
|
||||
onLanguageChange = (movieFileIds, languageId) => {
|
||||
const language = _.find(this.props.languages, { id: languageId });
|
||||
// TODO - Placeholder till we implement selection of multiple languages
|
||||
const languages = [language];
|
||||
this.props.dispatchUpdateMovieFiles({ movieFileIds, languages });
|
||||
}
|
||||
|
||||
onQualityChange = (movieFileIds, qualityId) => {
|
||||
const quality = {
|
||||
quality: _.find(this.props.qualities, { id: qualityId }),
|
||||
revision: {
|
||||
@ -72,31 +88,34 @@ class MovieFileEditorModalContentConnector extends Component {
|
||||
}
|
||||
};
|
||||
|
||||
this.props.dispatchUpdateMovieFiles({ episodeFileIds, quality });
|
||||
this.props.dispatchUpdateMovieFiles({ movieFileIds, quality });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchLanguages,
|
||||
dispatchFetchQualityProfileSchema,
|
||||
dispatchUpdateMovieFiles,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<MovieFileEditorModalContent
|
||||
<MovieFileEditorTableContent
|
||||
{...otherProps}
|
||||
onLanguageChange={this.onLanguageChange}
|
||||
onQualityChange={this.onQualityChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieFileEditorModalContentConnector.propTypes = {
|
||||
MovieFileEditorTableContentConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
dispatchUpdateMovieFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieFileEditorModalContentConnector);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieFileEditorTableContentConnector);
|
@ -0,0 +1,7 @@
|
||||
.placeholder {
|
||||
display: inline-block;
|
||||
margin: -8px 0;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
border: 2px dashed $dangerColor;
|
||||
}
|
10
frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.js
Normal file
10
frontend/src/MovieFile/Editor/MovieFileRowCellPlaceholder.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import styles from './MovieFileRowCellPlaceholder.css';
|
||||
|
||||
function MovieFileRowCellPlaceholder() {
|
||||
return (
|
||||
<span className={styles.placeholder} />
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieFileRowCellPlaceholder;
|
37
frontend/src/MovieFile/Language/SelectLanguageModal.js
Normal file
37
frontend/src/MovieFile/Language/SelectLanguageModal.js
Normal 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;
|
@ -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;
|
@ -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);
|
@ -6,10 +6,10 @@ import MediaInfo from './MediaInfo';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieFileSelector(),
|
||||
(episodeFile) => {
|
||||
if (episodeFile) {
|
||||
(movieFile) => {
|
||||
if (movieFile) {
|
||||
return {
|
||||
...episodeFile.mediaInfo
|
||||
...movieFile.mediaInfo
|
||||
};
|
||||
}
|
||||
|
||||
|
37
frontend/src/MovieFile/Quality/SelectQualityModal.js
Normal file
37
frontend/src/MovieFile/Quality/SelectQualityModal.js
Normal 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;
|
166
frontend/src/MovieFile/Quality/SelectQualityModalContent.js
Normal file
166
frontend/src/MovieFile/Quality/SelectQualityModalContent.js
Normal 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;
|
@ -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);
|
@ -38,9 +38,16 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: 'Language',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
@ -42,11 +42,13 @@ export const defaultState = {
|
||||
{
|
||||
name: 'language',
|
||||
label: 'Language',
|
||||
isVisible: false
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
@ -137,9 +137,10 @@ export const actionHandlers = handleThunks({
|
||||
},
|
||||
|
||||
[UPDATE_MOVIE_FILES]: function(getState, payload, dispatch) {
|
||||
|
||||
const {
|
||||
movieFileIds,
|
||||
language,
|
||||
languages,
|
||||
quality
|
||||
} = payload;
|
||||
|
||||
@ -149,8 +150,8 @@ export const actionHandlers = handleThunks({
|
||||
movieFileIds
|
||||
};
|
||||
|
||||
if (language) {
|
||||
data.language = language;
|
||||
if (languages) {
|
||||
data.languages = languages;
|
||||
}
|
||||
|
||||
if (quality) {
|
||||
@ -169,8 +170,8 @@ export const actionHandlers = handleThunks({
|
||||
...movieFileIds.map((id) => {
|
||||
const props = {};
|
||||
|
||||
if (language) {
|
||||
props.language = language;
|
||||
if (languages) {
|
||||
props.languages = languages;
|
||||
}
|
||||
|
||||
if (quality) {
|
||||
|
@ -23,27 +23,27 @@ export const defaultState = {
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_SERIES_HISTORY = 'seriesHistory/fetchMovieHistory';
|
||||
export const CLEAR_SERIES_HISTORY = 'seriesHistory/clearMovieHistory';
|
||||
export const SERIES_HISTORY_MARK_AS_FAILED = 'seriesHistory/seriesHistoryMarkAsFailed';
|
||||
export const FETCH_MOVIE_HISTORY = 'movieHistory/fetchMovieHistory';
|
||||
export const CLEAR_MOVIE_HISTORY = 'movieHistory/clearMovieHistory';
|
||||
export const MOVIE_HISTORY_MARK_AS_FAILED = 'movieHistory/movieHistoryMarkAsFailed';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchMovieHistory = createThunk(FETCH_SERIES_HISTORY);
|
||||
export const clearMovieHistory = createAction(CLEAR_SERIES_HISTORY);
|
||||
export const seriesHistoryMarkAsFailed = createThunk(SERIES_HISTORY_MARK_AS_FAILED);
|
||||
export const fetchMovieHistory = createThunk(FETCH_MOVIE_HISTORY);
|
||||
export const clearMovieHistory = createAction(CLEAR_MOVIE_HISTORY);
|
||||
export const movieHistoryMarkAsFailed = createThunk(MOVIE_HISTORY_MARK_AS_FAILED);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[FETCH_SERIES_HISTORY]: function(getState, payload, dispatch) {
|
||||
[FETCH_MOVIE_HISTORY]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isFetching: true }));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/history/series',
|
||||
url: '/history/movie',
|
||||
data: payload
|
||||
}).request;
|
||||
|
||||
@ -70,11 +70,10 @@ export const actionHandlers = handleThunks({
|
||||
});
|
||||
},
|
||||
|
||||
[SERIES_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) {
|
||||
[MOVIE_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
historyId,
|
||||
seriesId,
|
||||
seasonNumber
|
||||
movieId
|
||||
} = payload;
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
@ -86,7 +85,7 @@ export const actionHandlers = handleThunks({
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
dispatch(fetchMovieHistory({ seriesId, seasonNumber }));
|
||||
dispatch(fetchMovieHistory({ movieId }));
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -96,7 +95,7 @@ export const actionHandlers = handleThunks({
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_SERIES_HISTORY]: (state) => {
|
||||
[CLEAR_MOVIE_HISTORY]: (state) => {
|
||||
return Object.assign({}, state, defaultState);
|
||||
}
|
||||
|
||||
|
74
frontend/src/Store/Actions/movieTitlesActions.js
Normal file
74
frontend/src/Store/Actions/movieTitlesActions.js
Normal 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);
|
@ -68,6 +68,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: 'Languages',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
|
@ -11,8 +11,6 @@ import createHandleActions from './Creators/createHandleActions';
|
||||
// Variables
|
||||
|
||||
export const section = 'releases';
|
||||
export const episodeSection = 'releases.episode';
|
||||
export const seasonSection = 'releases.season';
|
||||
|
||||
let abortCurrentRequest = null;
|
||||
|
||||
@ -54,28 +52,6 @@ export const defaultState = {
|
||||
key: 'all',
|
||||
label: 'All',
|
||||
filters: []
|
||||
},
|
||||
{
|
||||
key: 'season-pack',
|
||||
label: 'Season Pack',
|
||||
filters: [
|
||||
{
|
||||
key: 'fullSeason',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'not-season-pack',
|
||||
label: 'Not Season Pack',
|
||||
filters: [
|
||||
{
|
||||
key: 'fullSeason',
|
||||
value: false,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@ -146,20 +122,13 @@ export const defaultState = {
|
||||
type: filterBuilderTypes.NUMBER
|
||||
}
|
||||
],
|
||||
selectedFilterKey: 'all'
|
||||
|
||||
episode: {
|
||||
selectedFilterKey: 'all'
|
||||
},
|
||||
|
||||
season: {
|
||||
selectedFilterKey: 'season-pack'
|
||||
}
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'releases.selectedFilterKey',
|
||||
'releases.episode.customFilters',
|
||||
'releases.season.customFilters'
|
||||
'releases.customFilters',
|
||||
'releases.selectedFilterKey'
|
||||
];
|
||||
|
||||
//
|
||||
@ -171,8 +140,7 @@ export const SET_RELEASES_SORT = 'releases/setReleasesSort';
|
||||
export const CLEAR_RELEASES = 'releases/clearReleases';
|
||||
export const GRAB_RELEASE = 'releases/grabRelease';
|
||||
export const UPDATE_RELEASE = 'releases/updateRelease';
|
||||
export const SET_EPISODE_RELEASES_FILTER = 'releases/setEpisodeReleasesFilter';
|
||||
export const SET_SEASON_RELEASES_FILTER = 'releases/setSeasonReleasesFilter';
|
||||
export const SET_RELEASES_FILTER = 'releases/setMovieReleasesFilter';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
@ -183,8 +151,7 @@ export const setReleasesSort = createAction(SET_RELEASES_SORT);
|
||||
export const clearReleases = createAction(CLEAR_RELEASES);
|
||||
export const grabRelease = createThunk(GRAB_RELEASE);
|
||||
export const updateRelease = createAction(UPDATE_RELEASE);
|
||||
export const setEpisodeReleasesFilter = createAction(SET_EPISODE_RELEASES_FILTER);
|
||||
export const setSeasonReleasesFilter = createAction(SET_SEASON_RELEASES_FILTER);
|
||||
export const setReleasesFilter = createAction(SET_RELEASES_FILTER);
|
||||
|
||||
//
|
||||
// Helpers
|
||||
@ -248,13 +215,7 @@ export const actionHandlers = handleThunks({
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_RELEASES]: (state) => {
|
||||
const {
|
||||
episode,
|
||||
season,
|
||||
...otherDefaultState
|
||||
} = defaultState;
|
||||
|
||||
return Object.assign({}, state, otherDefaultState);
|
||||
return Object.assign({}, state, defaultState);
|
||||
},
|
||||
|
||||
[UPDATE_RELEASE]: (state, { payload }) => {
|
||||
@ -276,8 +237,7 @@ export const reducers = createHandleActions({
|
||||
return newState;
|
||||
},
|
||||
|
||||
[SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||
[SET_EPISODE_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(episodeSection),
|
||||
[SET_SEASON_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(seasonSection)
|
||||
[SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section),
|
||||
[SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||
|
||||
}, defaultState, section);
|
||||
|
@ -148,7 +148,7 @@ private IEnumerable<DownloadDecision> GetMovieDecisions(List<ReleaseInfo> report
|
||||
}
|
||||
else
|
||||
{
|
||||
//remoteMovie.DownloadAllowed = true;
|
||||
remoteMovie.DownloadAllowed = true;
|
||||
decision = GetDecisionForReport(remoteMovie, searchCriteria);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace Radarr.Api.V2.MovieFiles
|
||||
@ -6,6 +7,7 @@ namespace Radarr.Api.V2.MovieFiles
|
||||
public class MovieFileListResource
|
||||
{
|
||||
public List<int> MovieFileIds { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -42,10 +42,10 @@ public MovieFileModule(IBroadcastSignalRMessage signalRBroadcaster,
|
||||
|
||||
GetResourceById = GetMovieFile;
|
||||
GetResourceAll = GetMovieFiles;
|
||||
UpdateResource = SetQuality;
|
||||
UpdateResource = SetMovieFile;
|
||||
DeleteResource = DeleteMovieFile;
|
||||
|
||||
Put["/editor"] = movieFiles => SetQuality();
|
||||
Put["/editor"] = movieFiles => SetMovieFile();
|
||||
Delete["/bulk"] = movieFiles => DeleteMovieFiles();
|
||||
}
|
||||
|
||||
@ -92,14 +92,15 @@ private List<MovieFileResource> GetMovieFiles()
|
||||
}
|
||||
}
|
||||
|
||||
private void SetQuality(MovieFileResource movieFileResource)
|
||||
private void SetMovieFile(MovieFileResource movieFileResource)
|
||||
{
|
||||
var movieFile = _mediaFileService.GetMovie(movieFileResource.Id);
|
||||
movieFile.Quality = movieFileResource.Quality;
|
||||
movieFile.Languages = movieFileResource.Languages;
|
||||
_mediaFileService.Update(movieFile);
|
||||
}
|
||||
|
||||
private Response SetQuality()
|
||||
private Response SetMovieFile()
|
||||
{
|
||||
var resource = Request.Body.FromJson<MovieFileListResource>();
|
||||
var movieFiles = _mediaFileService.GetMovies(resource.MovieFileIds);
|
||||
@ -111,6 +112,11 @@ private Response SetQuality()
|
||||
{
|
||||
movieFile.Quality = resource.Quality;
|
||||
}
|
||||
if (resource.Languages != null)
|
||||
{
|
||||
movieFile.Languages = resource.Languages;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_mediaFileService.Update(movieFiles);
|
||||
|
Loading…
Reference in New Issue
Block a user