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
pull/2/head
devbrian 5 years ago committed by Qstick
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,

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

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

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

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

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

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

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

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

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

@ -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;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell>
{relativePath}
</TableRowCell>
<RelativeDateCellConnector
date={airDateUtc}
/>
<TableRowCell>
<Label>
{language.name}
</Label>
</TableRowCell>
<TableRowCell>
<MovieQuality
quality={quality}
class MovieFileEditorRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isSelectQualityModalOpen: false,
isSelectLanguageModalOpen: false,
isConfirmDeleteModalOpen: false
};
}
//
// Listeners
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}
/>
<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}
/>
</TableRowCell>
</TableRow>
);
<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;

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

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

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

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

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

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

@ -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 @@ namespace NzbDrone.Core.DecisionEngine
}
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 @@ namespace Radarr.Api.V2.MovieFiles
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 @@ namespace Radarr.Api.V2.MovieFiles
}
}
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 @@ namespace Radarr.Api.V2.MovieFiles
{
movieFile.Quality = resource.Quality;
}
if (resource.Languages != null)
{
movieFile.Languages = resource.Languages;
}
}
_mediaFileService.Update(movieFiles);

Loading…
Cancel
Save