parent
9798202589
commit
152f50a1ef
@ -0,0 +1,6 @@
|
||||
.movieSearchCell {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
white-space: nowrap;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'movieSearchCell': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,81 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieInteractiveSearchModalConnector from './Search/MovieInteractiveSearchModalConnector';
|
||||
import styles from './MovieSearchCell.css';
|
||||
|
||||
class MovieSearchCell extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isInteractiveSearchModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onManualSearchPress = () => {
|
||||
this.setState({ isInteractiveSearchModalOpen: true });
|
||||
};
|
||||
|
||||
onInteractiveSearchModalClose = () => {
|
||||
this.setState({ isInteractiveSearchModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
movieId,
|
||||
movieTitle,
|
||||
isSearching,
|
||||
onSearchPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.movieSearchCell}>
|
||||
<SpinnerIconButton
|
||||
name={icons.SEARCH}
|
||||
isSpinning={isSearching}
|
||||
onPress={onSearchPress}
|
||||
title={translate('AutomaticSearch')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={this.onManualSearchPress}
|
||||
title={translate('InteractiveSearch')}
|
||||
/>
|
||||
|
||||
<MovieInteractiveSearchModalConnector
|
||||
isOpen={this.state.isInteractiveSearchModalOpen}
|
||||
movieId={movieId}
|
||||
movieTitle={movieTitle}
|
||||
onModalClose={this.onInteractiveSearchModalClose}
|
||||
{...otherProps}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieSearchCell.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
movieTitle: PropTypes.string.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieSearchCell;
|
@ -0,0 +1,48 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { movieId }) => movieId,
|
||||
createMovieSelector(),
|
||||
createCommandsSelector(),
|
||||
(movieId, movie, commands) => {
|
||||
const isSearching = commands.some((command) => {
|
||||
const movieSearch = command.name === commandNames.MOVIE_SEARCH;
|
||||
|
||||
if (!movieSearch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
isCommandExecuting(command) &&
|
||||
command.body.movieIds.indexOf(movieId) > -1
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
movieMonitored: movie.monitored,
|
||||
isSearching
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onSearchPress(name, path) {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: [props.movieId]
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieSearchCell);
|
@ -0,0 +1,4 @@
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'center': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,115 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||
import Icon from 'Components/Icon';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieQuality from './MovieQuality';
|
||||
import styles from './MovieStatus.css';
|
||||
|
||||
function MovieStatus(props) {
|
||||
const {
|
||||
isAvailable,
|
||||
monitored,
|
||||
grabbed,
|
||||
queueItem,
|
||||
movieFile
|
||||
} = props;
|
||||
|
||||
const hasMovieFile = !!movieFile;
|
||||
const isQueued = !!queueItem;
|
||||
|
||||
if (isQueued) {
|
||||
const {
|
||||
sizeleft,
|
||||
size
|
||||
} = queueItem;
|
||||
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<QueueDetails
|
||||
{...queueItem}
|
||||
progressBar={
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
kind={kinds.PURPLE}
|
||||
size={sizes.MEDIUM}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (grabbed) {
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
title={translate('MovieIsDownloading')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasMovieFile) {
|
||||
const quality = movieFile.quality;
|
||||
const isCutoffNotMet = movieFile.qualityCutoffNotMet;
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
size={movieFile.size}
|
||||
isCutoffNotMet={isCutoffNotMet}
|
||||
title={translate('MovieDownloaded')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!monitored) {
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.UNMONITORED}
|
||||
kind={kinds.DISABLED}
|
||||
title={translate('MovieIsNotMonitored')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAvailable) {
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.MISSING}
|
||||
title={translate('MovieMissingFromDisk')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.NOT_AIRED}
|
||||
title={translate('MovieIsNotAvailable')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MovieStatus.propTypes = {
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
grabbed: PropTypes.bool,
|
||||
queueItem: PropTypes.object,
|
||||
movieFile: PropTypes.object
|
||||
};
|
||||
|
||||
export default MovieStatus;
|
@ -0,0 +1,50 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import MovieStatus from 'Movie/MovieStatus';
|
||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
||||
import { createMovieByEntitySelector } from 'Store/Selectors/createMovieSelector';
|
||||
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieByEntitySelector(),
|
||||
createQueueItemSelector(),
|
||||
createMovieFileSelector(),
|
||||
(movie, queueItem, movieFile) => {
|
||||
const result = _.pick(movie, [
|
||||
'isAvailable',
|
||||
'monitored',
|
||||
'grabbed'
|
||||
]);
|
||||
|
||||
result.queueItem = queueItem;
|
||||
result.movieFile = movieFile;
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class MovieStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MovieStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieStatusConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
movieFileId: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, null)(MovieStatusConnector);
|
@ -1,9 +1,13 @@
|
||||
export const CALENDAR = 'calendar';
|
||||
export const MOVIES = 'movies';
|
||||
export const INTERACTIVE_IMPORT = 'interactiveImport.movies';
|
||||
export const WANTED_CUTOFF_UNMET = 'wanted.cutoffUnmet';
|
||||
export const WANTED_MISSING = 'wanted.missing';
|
||||
|
||||
export default {
|
||||
CALENDAR,
|
||||
MOVIES,
|
||||
INTERACTIVE_IMPORT
|
||||
INTERACTIVE_IMPORT,
|
||||
WANTED_CUTOFF_UNMET,
|
||||
WANTED_MISSING
|
||||
};
|
||||
|
@ -0,0 +1,298 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { filterTypes, sortDirections } from 'Helpers/Props';
|
||||
import createBatchToggleMovieMonitoredHandler from 'Store/Actions/Creators/createBatchToggleMovieMonitoredHandler';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
|
||||
import createClearReducer from './Creators/Reducers/createClearReducer';
|
||||
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'wanted';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
missing: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
pageSize: 20,
|
||||
sortKey: 'movieMetadata.sortTitle',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
error: null,
|
||||
items: [],
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'movieMetadata.sortTitle',
|
||||
label: () => translate('MovieTitle'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'movieMetadata.year',
|
||||
label: () => translate('Year'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: () => translate('Status'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: () => translate('Actions'),
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
}
|
||||
],
|
||||
|
||||
selectedFilterKey: 'monitored',
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
label: () => translate('Monitored'),
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'unmonitored',
|
||||
label: () => translate('Unmonitored'),
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: false,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
cutoffUnmet: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
pageSize: 20,
|
||||
sortKey: 'movieMetadata.sortTitle',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
items: [],
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'movieMetadata.sortTitle',
|
||||
label: () => translate('MovieTitle'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'movieMetadata.year',
|
||||
label: () => translate('Year'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: () => translate('Status'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: () => translate('Actions'),
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
}
|
||||
],
|
||||
|
||||
selectedFilterKey: 'monitored',
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
label: () => translate('Monitored'),
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: true,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'unmonitored',
|
||||
label: () => translate('Unmonitored'),
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: false,
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'wanted.missing.pageSize',
|
||||
'wanted.missing.sortKey',
|
||||
'wanted.missing.sortDirection',
|
||||
'wanted.missing.selectedFilterKey',
|
||||
'wanted.missing.columns',
|
||||
'wanted.cutoffUnmet.pageSize',
|
||||
'wanted.cutoffUnmet.sortKey',
|
||||
'wanted.cutoffUnmet.sortDirection',
|
||||
'wanted.cutoffUnmet.selectedFilterKey',
|
||||
'wanted.cutoffUnmet.columns'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_MISSING = 'wanted/missing/fetchMissing';
|
||||
export const GOTO_FIRST_MISSING_PAGE = 'wanted/missing/gotoMissingFirstPage';
|
||||
export const GOTO_PREVIOUS_MISSING_PAGE = 'wanted/missing/gotoMissingPreviousPage';
|
||||
export const GOTO_NEXT_MISSING_PAGE = 'wanted/missing/gotoMissingNextPage';
|
||||
export const GOTO_LAST_MISSING_PAGE = 'wanted/missing/gotoMissingLastPage';
|
||||
export const GOTO_MISSING_PAGE = 'wanted/missing/gotoMissingPage';
|
||||
export const SET_MISSING_SORT = 'wanted/missing/setMissingSort';
|
||||
export const SET_MISSING_FILTER = 'wanted/missing/setMissingFilter';
|
||||
export const SET_MISSING_TABLE_OPTION = 'wanted/missing/setMissingTableOption';
|
||||
export const CLEAR_MISSING = 'wanted/missing/clearMissing';
|
||||
|
||||
export const BATCH_TOGGLE_MISSING_MOVIES = 'wanted/missing/batchToggleMissingMovies';
|
||||
|
||||
export const FETCH_CUTOFF_UNMET = 'wanted/cutoffUnmet/fetchCutoffUnmet';
|
||||
export const GOTO_FIRST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFirstPage';
|
||||
export const GOTO_PREVIOUS_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPreviousPage';
|
||||
export const GOTO_NEXT_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetNextPage';
|
||||
export const GOTO_LAST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFastPage';
|
||||
export const GOTO_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPage';
|
||||
export const SET_CUTOFF_UNMET_SORT = 'wanted/cutoffUnmet/setCutoffUnmetSort';
|
||||
export const SET_CUTOFF_UNMET_FILTER = 'wanted/cutoffUnmet/setCutoffUnmetFilter';
|
||||
export const SET_CUTOFF_UNMET_TABLE_OPTION = 'wanted/cutoffUnmet/setCutoffUnmetTableOption';
|
||||
export const CLEAR_CUTOFF_UNMET = 'wanted/cutoffUnmet/clearCutoffUnmet';
|
||||
|
||||
export const BATCH_TOGGLE_CUTOFF_UNMET_MOVIES = 'wanted/cutoffUnmet/batchToggleCutoffUnmetMovies';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchMissing = createThunk(FETCH_MISSING);
|
||||
export const gotoMissingFirstPage = createThunk(GOTO_FIRST_MISSING_PAGE);
|
||||
export const gotoMissingPreviousPage = createThunk(GOTO_PREVIOUS_MISSING_PAGE);
|
||||
export const gotoMissingNextPage = createThunk(GOTO_NEXT_MISSING_PAGE);
|
||||
export const gotoMissingLastPage = createThunk(GOTO_LAST_MISSING_PAGE);
|
||||
export const gotoMissingPage = createThunk(GOTO_MISSING_PAGE);
|
||||
export const setMissingSort = createThunk(SET_MISSING_SORT);
|
||||
export const setMissingFilter = createThunk(SET_MISSING_FILTER);
|
||||
export const setMissingTableOption = createAction(SET_MISSING_TABLE_OPTION);
|
||||
export const clearMissing = createAction(CLEAR_MISSING);
|
||||
|
||||
export const batchToggleMissingMovies = createThunk(BATCH_TOGGLE_MISSING_MOVIES);
|
||||
|
||||
export const fetchCutoffUnmet = createThunk(FETCH_CUTOFF_UNMET);
|
||||
export const gotoCutoffUnmetFirstPage = createThunk(GOTO_FIRST_CUTOFF_UNMET_PAGE);
|
||||
export const gotoCutoffUnmetPreviousPage = createThunk(GOTO_PREVIOUS_CUTOFF_UNMET_PAGE);
|
||||
export const gotoCutoffUnmetNextPage = createThunk(GOTO_NEXT_CUTOFF_UNMET_PAGE);
|
||||
export const gotoCutoffUnmetLastPage = createThunk(GOTO_LAST_CUTOFF_UNMET_PAGE);
|
||||
export const gotoCutoffUnmetPage = createThunk(GOTO_CUTOFF_UNMET_PAGE);
|
||||
export const setCutoffUnmetSort = createThunk(SET_CUTOFF_UNMET_SORT);
|
||||
export const setCutoffUnmetFilter = createThunk(SET_CUTOFF_UNMET_FILTER);
|
||||
export const setCutoffUnmetTableOption = createAction(SET_CUTOFF_UNMET_TABLE_OPTION);
|
||||
export const clearCutoffUnmet = createAction(CLEAR_CUTOFF_UNMET);
|
||||
|
||||
export const batchToggleCutoffUnmetMovies = createThunk(BATCH_TOGGLE_CUTOFF_UNMET_MOVIES);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
...createServerSideCollectionHandlers(
|
||||
'wanted.missing',
|
||||
'/wanted/missing',
|
||||
fetchMissing,
|
||||
{
|
||||
[serverSideCollectionHandlers.FETCH]: FETCH_MISSING,
|
||||
[serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_MISSING_PAGE,
|
||||
[serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_MISSING_PAGE,
|
||||
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_MISSING_PAGE,
|
||||
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_MISSING_PAGE,
|
||||
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_MISSING_PAGE,
|
||||
[serverSideCollectionHandlers.SORT]: SET_MISSING_SORT,
|
||||
[serverSideCollectionHandlers.FILTER]: SET_MISSING_FILTER
|
||||
}
|
||||
),
|
||||
|
||||
[BATCH_TOGGLE_MISSING_MOVIES]: createBatchToggleMovieMonitoredHandler('wanted.missing', fetchMissing),
|
||||
|
||||
...createServerSideCollectionHandlers(
|
||||
'wanted.cutoffUnmet',
|
||||
'/wanted/cutoff',
|
||||
fetchCutoffUnmet,
|
||||
{
|
||||
[serverSideCollectionHandlers.FETCH]: FETCH_CUTOFF_UNMET,
|
||||
[serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_CUTOFF_UNMET_PAGE,
|
||||
[serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_CUTOFF_UNMET_PAGE,
|
||||
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_CUTOFF_UNMET_PAGE,
|
||||
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_CUTOFF_UNMET_PAGE,
|
||||
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_CUTOFF_UNMET_PAGE,
|
||||
[serverSideCollectionHandlers.SORT]: SET_CUTOFF_UNMET_SORT,
|
||||
[serverSideCollectionHandlers.FILTER]: SET_CUTOFF_UNMET_FILTER
|
||||
}
|
||||
),
|
||||
|
||||
[BATCH_TOGGLE_CUTOFF_UNMET_MOVIES]: createBatchToggleMovieMonitoredHandler('wanted.cutoffUnmet', fetchCutoffUnmet)
|
||||
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_MISSING_TABLE_OPTION]: createSetTableOptionReducer('wanted.missing'),
|
||||
[SET_CUTOFF_UNMET_TABLE_OPTION]: createSetTableOptionReducer('wanted.cutoffUnmet'),
|
||||
|
||||
[CLEAR_MISSING]: createClearReducer(
|
||||
'wanted.missing',
|
||||
{
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: [],
|
||||
totalPages: 0,
|
||||
totalRecords: 0
|
||||
}
|
||||
),
|
||||
|
||||
[CLEAR_CUTOFF_UNMET]: createClearReducer(
|
||||
'wanted.cutoffUnmet',
|
||||
{
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: [],
|
||||
totalPages: 0,
|
||||
totalRecords: 0
|
||||
}
|
||||
)
|
||||
|
||||
}, defaultState, section);
|
@ -0,0 +1,301 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
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 CutoffUnmetRow from './CutoffUnmetRow';
|
||||
|
||||
function getMonitoredValue(props) {
|
||||
const {
|
||||
filters,
|
||||
selectedFilterKey
|
||||
} = props;
|
||||
|
||||
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||
}
|
||||
|
||||
class CutoffUnmet extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmSearchAllCutoffUnmetModalOpen: false,
|
||||
isInteractiveImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState((state) => {
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFilterMenuItemPress = (filterKey, filterValue) => {
|
||||
this.props.onFilterSelect(filterKey, filterValue);
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
onSearchSelectedPress = () => {
|
||||
const selected = this.getSelectedIds();
|
||||
|
||||
this.props.onSearchSelectedPress(selected);
|
||||
};
|
||||
|
||||
onToggleSelectedPress = () => {
|
||||
const movieIds = this.getSelectedIds();
|
||||
|
||||
this.props.batchToggleCutoffUnmetMovies({
|
||||
movieIds,
|
||||
monitored: !getMonitoredValue(this.props)
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllCutoffUnmetPress = () => {
|
||||
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: true });
|
||||
};
|
||||
|
||||
onSearchAllCutoffUnmetConfirmed = () => {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
onSearchAllCutoffUnmetPress
|
||||
} = this.props;
|
||||
|
||||
// TODO: Custom filters will need to check whether there is a monitored
|
||||
// filter once implemented.
|
||||
|
||||
onSearchAllCutoffUnmetPress(selectedFilterKey === 'monitored');
|
||||
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmSearchAllCutoffUnmetModalClose = () => {
|
||||
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
columns,
|
||||
totalRecords,
|
||||
isSearchingForCutoffUnmetMovies,
|
||||
isSaving,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmSearchAllCutoffUnmetModalOpen
|
||||
} = this.state;
|
||||
|
||||
const itemsSelected = !!this.getSelectedIds().length;
|
||||
const isShowingMonitored = getMonitoredValue(this.props);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('CutoffUnmet')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('SearchSelected')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!itemsSelected || isSearchingForCutoffUnmetMovies}
|
||||
onPress={this.onSearchSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||
iconName={icons.MONITORED}
|
||||
isDisabled={!itemsSelected}
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onToggleSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('SearchAll')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isSearchingForCutoffUnmetMovies}
|
||||
onPress={this.onSearchAllCutoffUnmetPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('CutoffUnmetLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('CutoffUnmetNoItems')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<CutoffUnmetRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmSearchAllCutoffUnmetModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('SearchForCutoffUnmetMovies')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('SearchForCutoffUnmetMoviesConfirmationCount', { totalRecords })}
|
||||
</div>
|
||||
<div>
|
||||
{translate('MassSearchCancelWarning')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Search')}
|
||||
onConfirm={this.onSearchAllCutoffUnmetConfirmed}
|
||||
onCancel={this.onConfirmSearchAllCutoffUnmetModalClose}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CutoffUnmet.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isSearchingForCutoffUnmetMovies: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onSearchSelectedPress: PropTypes.func.isRequired,
|
||||
batchToggleCutoffUnmetMovies: PropTypes.func.isRequired,
|
||||
onSearchAllCutoffUnmetPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CutoffUnmet;
|
@ -0,0 +1,185 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import * as wantedActions from 'Store/Actions/wantedActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import CutoffUnmet from './CutoffUnmet';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.wanted.cutoffUnmet,
|
||||
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH),
|
||||
(cutoffUnmet, isSearchingForCutoffUnmetMovies) => {
|
||||
return {
|
||||
isSearchingForCutoffUnmetMovies,
|
||||
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
|
||||
...cutoffUnmet
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...wantedActions,
|
||||
executeCommand,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails,
|
||||
fetchMovieFiles,
|
||||
clearMovieFiles
|
||||
};
|
||||
|
||||
class CutoffUnmetConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchCutoffUnmet,
|
||||
gotoCutoffUnmetFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchCutoffUnmet();
|
||||
} else {
|
||||
gotoCutoffUnmetFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const movieIds = selectUniqueIds(this.props.items, 'id');
|
||||
const movieFileIds = selectUniqueIds(this.props.items, 'movieFileId');
|
||||
|
||||
this.props.fetchQueueDetails({ movieIds });
|
||||
|
||||
if (movieFileIds.length) {
|
||||
this.props.fetchMovieFiles({ movieFileIds });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearCutoffUnmet();
|
||||
this.props.clearQueueDetails();
|
||||
this.props.clearMovieFiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchCutoffUnmet();
|
||||
};
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoCutoffUnmetFirstPage();
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoCutoffUnmetPreviousPage();
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoCutoffUnmetNextPage();
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoCutoffUnmetLastPage();
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoCutoffUnmetPage({ page });
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setCutoffUnmetSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setCutoffUnmetFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setCutoffUnmetTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoCutoffUnmetFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: selected
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllCutoffUnmetPress = (monitored) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
|
||||
monitored
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CutoffUnmet
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onSearchSelectedPress={this.onSearchSelectedPress}
|
||||
onToggleSelectedPress={this.onToggleSelectedPress}
|
||||
onSearchAllCutoffUnmetPress={this.onSearchAllCutoffUnmetPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CutoffUnmetConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchCutoffUnmet: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetFirstPage: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetPreviousPage: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetNextPage: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetLastPage: PropTypes.func.isRequired,
|
||||
gotoCutoffUnmetPage: PropTypes.func.isRequired,
|
||||
setCutoffUnmetSort: PropTypes.func.isRequired,
|
||||
setCutoffUnmetFilter: PropTypes.func.isRequired,
|
||||
setCutoffUnmetTableOption: PropTypes.func.isRequired,
|
||||
clearCutoffUnmet: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired,
|
||||
fetchMovieFiles: PropTypes.func.isRequired,
|
||||
clearMovieFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(CutoffUnmetConnector)
|
||||
);
|
@ -0,0 +1,6 @@
|
||||
.languages,
|
||||
.status {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'languages': string;
|
||||
'status': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,120 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import movieEntities from 'Movie/movieEntities';
|
||||
import MovieSearchCellConnector from 'Movie/MovieSearchCellConnector';
|
||||
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import MovieFileLanguageConnector from 'MovieFile/MovieFileLanguageConnector';
|
||||
import styles from './CutoffUnmetRow.css';
|
||||
|
||||
function CutoffUnmetRow(props) {
|
||||
const {
|
||||
id,
|
||||
movieFileId,
|
||||
year,
|
||||
title,
|
||||
titleSlug,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
titleSlug={titleSlug}
|
||||
title={title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.year') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{year}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.languages}
|
||||
>
|
||||
<MovieFileLanguageConnector
|
||||
movieFileId={movieFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.status}
|
||||
>
|
||||
<MovieStatusConnector
|
||||
movieId={id}
|
||||
movieFileId={movieFileId}
|
||||
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<MovieSearchCellConnector
|
||||
key={name}
|
||||
movieId={id}
|
||||
movieTitle={title}
|
||||
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
CutoffUnmetRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movieFileId: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CutoffUnmetRow;
|
@ -0,0 +1,319 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
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 MissingRow from './MissingRow';
|
||||
|
||||
function getMonitoredValue(props) {
|
||||
const {
|
||||
filters,
|
||||
selectedFilterKey
|
||||
} = props;
|
||||
|
||||
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||
}
|
||||
|
||||
class Missing extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmSearchAllMissingModalOpen: false,
|
||||
isInteractiveImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState((state) => {
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// 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);
|
||||
});
|
||||
};
|
||||
|
||||
onSearchSelectedPress = () => {
|
||||
const selected = this.getSelectedIds();
|
||||
|
||||
this.props.onSearchSelectedPress(selected);
|
||||
};
|
||||
|
||||
onToggleSelectedPress = () => {
|
||||
const movieIds = this.getSelectedIds();
|
||||
|
||||
this.props.batchToggleMissingMovies({
|
||||
movieIds,
|
||||
monitored: !getMonitoredValue(this.props)
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllMissingPress = () => {
|
||||
this.setState({ isConfirmSearchAllMissingModalOpen: true });
|
||||
};
|
||||
|
||||
onSearchAllMissingConfirmed = () => {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
onSearchAllMissingPress
|
||||
} = this.props;
|
||||
|
||||
// TODO: Custom filters will need to check whether there is a monitored
|
||||
// filter once implemented.
|
||||
|
||||
onSearchAllMissingPress(selectedFilterKey === 'monitored');
|
||||
this.setState({ isConfirmSearchAllMissingModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmSearchAllMissingModalClose = () => {
|
||||
this.setState({ isConfirmSearchAllMissingModalOpen: false });
|
||||
};
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
};
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
columns,
|
||||
totalRecords,
|
||||
isSearchingForMissingMovies,
|
||||
isSaving,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmSearchAllMissingModalOpen,
|
||||
isInteractiveImportModalOpen
|
||||
} = this.state;
|
||||
|
||||
const itemsSelected = !!this.getSelectedIds().length;
|
||||
const isShowingMonitored = getMonitoredValue(this.props);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Missing')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('SearchSelected')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!itemsSelected || isSearchingForMissingMovies}
|
||||
onPress={this.onSearchSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||
iconName={icons.MONITORED}
|
||||
isDisabled={!itemsSelected}
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onToggleSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('SearchAll')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!items.length}
|
||||
isSpinning={isSearchingForMissingMovies}
|
||||
onPress={this.onSearchAllMissingPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManualImport')}
|
||||
iconName={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('MissingLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('MissingNoItems')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<MissingRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmSearchAllMissingModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('SearchForAllMissingMovies')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('SearchForAllMissingMoviesConfirmationCount', { totalRecords })}
|
||||
</div>
|
||||
<div>
|
||||
{translate('MassSearchCancelWarning')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
confirmLabel={translate('Search')}
|
||||
onConfirm={this.onSearchAllMissingConfirmed}
|
||||
onCancel={this.onConfirmSearchAllMissingModalClose}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Missing.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isSearchingForMissingMovies: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onSearchSelectedPress: PropTypes.func.isRequired,
|
||||
batchToggleMissingMovies: PropTypes.func.isRequired,
|
||||
onSearchAllMissingPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Missing;
|
@ -0,0 +1,173 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import * as wantedActions from 'Store/Actions/wantedActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Missing from './Missing';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.wanted.missing,
|
||||
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH),
|
||||
(missing, isSearchingForMissingMovies) => {
|
||||
return {
|
||||
isSearchingForMissingMovies,
|
||||
isSaving: missing.items.filter((m) => m.isSaving).length > 1,
|
||||
...missing
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...wantedActions,
|
||||
executeCommand,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails
|
||||
};
|
||||
|
||||
class MissingConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchMissing,
|
||||
gotoMissingFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchMissing();
|
||||
} else {
|
||||
gotoMissingFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const movieIds = selectUniqueIds(this.props.items, 'id');
|
||||
this.props.fetchQueueDetails({ movieIds });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearMissing();
|
||||
this.props.clearQueueDetails();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchMissing();
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoMissingFirstPage();
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoMissingPreviousPage();
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoMissingNextPage();
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoMissingLastPage();
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoMissingPage({ page });
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setMissingSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setMissingFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setMissingTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoMissingFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: selected
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllMissingPress = (monitored) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MISSING_MOVIES_SEARCH,
|
||||
monitored
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Missing
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onSearchSelectedPress={this.onSearchSelectedPress}
|
||||
onSearchAllMissingPress={this.onSearchAllMissingPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MissingConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchMissing: PropTypes.func.isRequired,
|
||||
gotoMissingFirstPage: PropTypes.func.isRequired,
|
||||
gotoMissingPreviousPage: PropTypes.func.isRequired,
|
||||
gotoMissingNextPage: PropTypes.func.isRequired,
|
||||
gotoMissingLastPage: PropTypes.func.isRequired,
|
||||
gotoMissingPage: PropTypes.func.isRequired,
|
||||
setMissingSort: PropTypes.func.isRequired,
|
||||
setMissingFilter: PropTypes.func.isRequired,
|
||||
setMissingTableOption: PropTypes.func.isRequired,
|
||||
clearMissing: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(MissingConnector)
|
||||
);
|
@ -0,0 +1,5 @@
|
||||
.status {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'status': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,110 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import movieEntities from 'Movie/movieEntities';
|
||||
import MovieSearchCellConnector from 'Movie/MovieSearchCellConnector';
|
||||
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import styles from './MissingRow.css';
|
||||
|
||||
function MissingRow(props) {
|
||||
const {
|
||||
id,
|
||||
movieFileId,
|
||||
year,
|
||||
title,
|
||||
titleSlug,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange
|
||||
} = props;
|
||||
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
titleSlug={titleSlug}
|
||||
title={title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movieMetadata.year') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{year}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.status}
|
||||
>
|
||||
<MovieStatusConnector
|
||||
movieId={id}
|
||||
movieFileId={movieFileId}
|
||||
movieEntity={movieEntities.WANTED_MISSING}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<MovieSearchCellConnector
|
||||
key={name}
|
||||
movieId={id}
|
||||
movieTitle={title}
|
||||
movieEntity={movieEntities.WANTED_MISSING}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
MissingRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movieFileId: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MissingRow;
|
@ -0,0 +1,159 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.MediaFiles.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Movies.Translations;
|
||||
using NzbDrone.Core.MovieStats;
|
||||
using NzbDrone.SignalR;
|
||||
using Radarr.Http.REST;
|
||||
|
||||
namespace Radarr.Api.V3.Movies
|
||||
{
|
||||
public abstract class MovieControllerWithSignalR : RestControllerWithSignalR<MovieResource, Movie>,
|
||||
IHandle<MovieGrabbedEvent>,
|
||||
IHandle<MovieFileImportedEvent>,
|
||||
IHandle<MovieFileDeletedEvent>
|
||||
{
|
||||
protected readonly IMovieService _movieService;
|
||||
protected readonly IMovieTranslationService _movieTranslationService;
|
||||
protected readonly IMovieStatisticsService _movieStatisticsService;
|
||||
protected readonly IUpgradableSpecification _upgradableSpecification;
|
||||
protected readonly ICustomFormatCalculationService _formatCalculator;
|
||||
protected readonly IConfigService _configService;
|
||||
|
||||
protected MovieControllerWithSignalR(IMovieService movieService,
|
||||
IMovieTranslationService movieTranslationService,
|
||||
IMovieStatisticsService movieStatisticsService,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IConfigService configService,
|
||||
IBroadcastSignalRMessage signalRBroadcaster)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_movieService = movieService;
|
||||
_movieTranslationService = movieTranslationService;
|
||||
_movieStatisticsService = movieStatisticsService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_formatCalculator = formatCalculator;
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
protected MovieControllerWithSignalR(IMovieService movieService,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IBroadcastSignalRMessage signalRBroadcaster,
|
||||
string resource)
|
||||
: base(signalRBroadcaster)
|
||||
{
|
||||
_movieService = movieService;
|
||||
_upgradableSpecification = upgradableSpecification;
|
||||
_formatCalculator = formatCalculator;
|
||||
}
|
||||
|
||||
protected override MovieResource GetResourceById(int id)
|
||||
{
|
||||
var movie = _movieService.GetMovie(id);
|
||||
var resource = MapToResource(movie);
|
||||
return resource;
|
||||
}
|
||||
|
||||
protected MovieResource MapToResource(Movie movie)
|
||||
{
|
||||
if (movie == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var availDelay = _configService.AvailabilityDelay;
|
||||
var language = (Language)_configService.MovieInfoLanguage;
|
||||
|
||||
var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId);
|
||||
var translation = GetMovieTranslation(translations, movie.MovieMetadata, language);
|
||||
|
||||
var resource = movie.ToResource(availDelay, translation, _upgradableSpecification, _formatCalculator);
|
||||
FetchAndLinkMovieStatistics(resource);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
protected List<MovieResource> MapToResource(List<Movie> movies)
|
||||
{
|
||||
var resources = new List<MovieResource>();
|
||||
var availDelay = _configService.AvailabilityDelay;
|
||||
var language = (Language)_configService.MovieInfoLanguage;
|
||||
|
||||
foreach (var movie in movies)
|
||||
{
|
||||
if (movie == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId);
|
||||
var translation = GetMovieTranslation(translations, movie.MovieMetadata, language);
|
||||
|
||||
var resource = movie.ToResource(availDelay, translation, _upgradableSpecification, _formatCalculator);
|
||||
FetchAndLinkMovieStatistics(resource);
|
||||
|
||||
resources.Add(resource);
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
private MovieTranslation GetMovieTranslation(List<MovieTranslation> translations, MovieMetadata movie, Language language)
|
||||
{
|
||||
if (language == Language.Original)
|
||||
{
|
||||
return new MovieTranslation
|
||||
{
|
||||
Title = movie.OriginalTitle,
|
||||
Overview = movie.Overview
|
||||
};
|
||||
}
|
||||
|
||||
return translations.FirstOrDefault(t => t.Language == language && t.MovieMetadataId == movie.Id);
|
||||
}
|
||||
|
||||
private void FetchAndLinkMovieStatistics(MovieResource resource)
|
||||
{
|
||||
LinkMovieStatistics(resource, _movieStatisticsService.MovieStatistics(resource.Id));
|
||||
}
|
||||
|
||||
private void LinkMovieStatistics(MovieResource resource, MovieStatistics movieStatistics)
|
||||
{
|
||||
resource.Statistics = movieStatistics.ToResource();
|
||||
resource.HasFile = movieStatistics.MovieFileCount > 0;
|
||||
resource.SizeOnDisk = movieStatistics.SizeOnDisk;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(MovieGrabbedEvent message)
|
||||
{
|
||||
var resource = message.Movie.Movie.ToResource(0, null, _upgradableSpecification, _formatCalculator);
|
||||
resource.Grabbed = true;
|
||||
|
||||
BroadcastResourceChange(ModelAction.Updated, resource);
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(MovieFileImportedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.MovieInfo.Movie.Id);
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
public void Handle(MovieFileDeletedEvent message)
|
||||
{
|
||||
BroadcastResourceChange(ModelAction.Updated, message.MovieFile.Movie.Id);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Movies.Translations;
|
||||
using NzbDrone.Core.MovieStats;
|
||||
using NzbDrone.SignalR;
|
||||
using Radarr.Api.V3.Movies;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.Extensions;
|
||||
|
||||
namespace Radarr.Api.V3.Wanted
|
||||
{
|
||||
[V3ApiController("wanted/cutoff")]
|
||||
public class CutoffController : MovieControllerWithSignalR
|
||||
{
|
||||
private readonly IMovieCutoffService _movieCutoffService;
|
||||
|
||||
public CutoffController(IMovieCutoffService movieCutoffService,
|
||||
IMovieService movieService,
|
||||
IMovieTranslationService movieTranslationService,
|
||||
IMovieStatisticsService movieStatisticsService,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IConfigService configService,
|
||||
IBroadcastSignalRMessage signalRBroadcaster)
|
||||
: base(movieService, movieTranslationService, movieStatisticsService, upgradableSpecification, formatCalculator, configService, signalRBroadcaster)
|
||||
{
|
||||
_movieCutoffService = movieCutoffService;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
protected override MovieResource GetResourceById(int id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public PagingResource<MovieResource> GetCutoffUnmetMovies([FromQuery] PagingRequestResource paging, bool monitored = true)
|
||||
{
|
||||
var pagingResource = new PagingResource<MovieResource>(paging);
|
||||
var pagingSpec = new PagingSpec<Movie>
|
||||
{
|
||||
Page = pagingResource.Page,
|
||||
PageSize = pagingResource.PageSize,
|
||||
SortKey = pagingResource.SortKey,
|
||||
SortDirection = pagingResource.SortDirection
|
||||
};
|
||||
|
||||
pagingSpec.FilterExpressions.Add(v => v.Monitored == monitored);
|
||||
|
||||
var resource = pagingSpec.ApplyToPage(_movieCutoffService.MoviesWhereCutoffUnmet, v => MapToResource(v));
|
||||
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Movies.Translations;
|
||||
using NzbDrone.Core.MovieStats;
|
||||
using NzbDrone.SignalR;
|
||||
using Radarr.Api.V3.Movies;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.Extensions;
|
||||
|
||||
namespace Radarr.Api.V3.Wanted
|
||||
{
|
||||
[V3ApiController("wanted/missing")]
|
||||
public class MissingController : MovieControllerWithSignalR
|
||||
{
|
||||
public MissingController(IMovieService movieService,
|
||||
IMovieTranslationService movieTranslationService,
|
||||
IMovieStatisticsService movieStatisticsService,
|
||||
IUpgradableSpecification upgradableSpecification,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IConfigService configService,
|
||||
IBroadcastSignalRMessage signalRBroadcaster)
|
||||
: base(movieService, movieTranslationService, movieStatisticsService, upgradableSpecification, formatCalculator, configService, signalRBroadcaster)
|
||||
{
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
protected override MovieResource GetResourceById(int id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public PagingResource<MovieResource> GetMissingMovies([FromQuery] PagingRequestResource paging, bool monitored = true)
|
||||
{
|
||||
var pagingResource = new PagingResource<MovieResource>(paging);
|
||||
var pagingSpec = new PagingSpec<Movie>
|
||||
{
|
||||
Page = pagingResource.Page,
|
||||
PageSize = pagingResource.PageSize,
|
||||
SortKey = pagingResource.SortKey,
|
||||
SortDirection = pagingResource.SortDirection
|
||||
};
|
||||
|
||||
pagingSpec.FilterExpressions.Add(v => v.Monitored == monitored);
|
||||
|
||||
var resource = pagingSpec.ApplyToPage(_movieService.MoviesWithoutFiles, v => MapToResource(v));
|
||||
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue