mirror of
https://github.com/Radarr/Radarr.git
synced 2024-09-11 20:12:41 +02:00
New: Movie Editor in Movie Index (#3606)
* Fixed: Movie Editor in Movie Index * Fixed: CSS Style Issues * Fixed: Ensure only items shown are selected * Fixed: Cleanup and Rename from Editor
This commit is contained in:
parent
b8f7ca0749
commit
a20222fbef
@ -61,7 +61,7 @@ const mapDispatchToProps = {
|
||||
dispatchImportMovie: importMovie,
|
||||
dispatchClearImportMovie: clearImportMovie,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchSetAddSeriesDefault: setAddMovieDefault
|
||||
dispatchSetAddMovieDefault: setAddMovieDefault
|
||||
};
|
||||
|
||||
class ImportMovieConnector extends Component {
|
||||
@ -74,7 +74,7 @@ class ImportMovieConnector extends Component {
|
||||
qualityProfiles,
|
||||
defaultQualityProfileId,
|
||||
dispatchFetchRootFolders,
|
||||
dispatchSetAddSeriesDefault
|
||||
dispatchSetAddMovieDefault
|
||||
} = this.props;
|
||||
|
||||
if (!this.props.rootFoldersPopulated) {
|
||||
@ -93,7 +93,7 @@ class ImportMovieConnector extends Component {
|
||||
}
|
||||
|
||||
if (setDefaults) {
|
||||
dispatchSetAddSeriesDefault(setDefaultPayload);
|
||||
dispatchSetAddMovieDefault(setDefaultPayload);
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ class ImportMovieConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onInputChange = (ids, name, value) => {
|
||||
this.props.dispatchSetAddSeriesDefault({ [name]: value });
|
||||
this.props.dispatchSetAddMovieDefault({ [name]: value });
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.dispatchSetImportMovieValue({
|
||||
@ -146,7 +146,7 @@ ImportMovieConnector.propTypes = {
|
||||
dispatchImportMovie: PropTypes.func.isRequired,
|
||||
dispatchClearImportMovie: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchSetAddSeriesDefault: PropTypes.func.isRequired
|
||||
dispatchSetAddMovieDefault: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieConnector);
|
||||
|
@ -5,8 +5,8 @@ import { cancelLookupMovie } from 'Store/Actions/importMovieActions';
|
||||
import ImportMovieFooter from './ImportMovieFooter';
|
||||
|
||||
function isMixed(items, selectedIds, defaultValue, key) {
|
||||
return _.some(items, (series) => {
|
||||
return selectedIds.indexOf(series.id) > -1 && series[key] !== defaultValue;
|
||||
return _.some(items, (movie) => {
|
||||
return selectedIds.indexOf(movie.id) > -1 && movie[key] !== defaultValue;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.series {
|
||||
.movie {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 400px;
|
||||
|
@ -53,7 +53,7 @@ function ImportMovieRow(props) {
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.series}>
|
||||
<VirtualTableRowCell className={styles.movie}>
|
||||
<ImportMovieSelectMovieConnector
|
||||
id={id}
|
||||
isExistingMovie={isExistingMovie}
|
||||
|
@ -66,23 +66,23 @@ class ImportMovieTable extends Component {
|
||||
const isExistingMovie = !!selectedMovie &&
|
||||
_.some(prevProps.allMovies, { tmdbId: selectedMovie.tmdbId });
|
||||
|
||||
// Props doesn't have a selected series or
|
||||
// the selected series is an existing series.
|
||||
// Props doesn't have a selected movie or
|
||||
// the selected movie is an existing movie.
|
||||
if ((!selectedMovie && prevItem.selectedMovie) || (isExistingMovie && !prevItem.selectedMovie)) {
|
||||
onSelectedChange({ id, value: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// State is selected, but a series isn't selected or
|
||||
// the selected series is an existing series.
|
||||
// State is selected, but a movie isn't selected or
|
||||
// the selected movie is an existing movie.
|
||||
if (isSelected && (!selectedMovie || isExistingMovie)) {
|
||||
onSelectedChange({ id, value: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A series is being selected that wasn't previously selected.
|
||||
// A movie is being selected that wasn't previously selected.
|
||||
if (selectedMovie && selectedMovie !== prevItem.selectedMovie) {
|
||||
onSelectedChange({ id, value: true });
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
.series {
|
||||
.movie {
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
|
@ -26,7 +26,7 @@ class ImportMovieSearchResult extends Component {
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.series}
|
||||
className={styles.movie}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportMovieTitle
|
||||
|
@ -259,7 +259,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
title={item.title}
|
||||
year={item.year}
|
||||
studio={item.studio}
|
||||
onPress={this.onSeriesSelect}
|
||||
onPress={this.onMovieSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
@ -7,7 +7,6 @@ import Switch from 'Components/Router/Switch';
|
||||
import MovieIndexConnector from 'Movie/Index/MovieIndexConnector';
|
||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||
import SeriesEditorConnector from 'Movie/Editor/SeriesEditorConnector';
|
||||
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
|
||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||
@ -77,11 +76,6 @@ function AppRoutes(props) {
|
||||
component={ImportMovies}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/serieseditor"
|
||||
component={SeriesEditorConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/movie/:titleSlug"
|
||||
component={MovieDetailsPageConnector}
|
||||
|
@ -163,7 +163,7 @@ class CalendarLinkModalContent extends Component {
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
value={tags}
|
||||
helpText="Feed will only contain series with at least one matching tag"
|
||||
helpText="Feed will only contain movies with at least one matching tag"
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -12,7 +12,7 @@ export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch';
|
||||
export const MOVE_MOVIE = 'MoveMovie';
|
||||
export const REFRESH_MOVIE = 'RefreshMovie';
|
||||
export const RENAME_FILES = 'RenameFiles';
|
||||
export const RENAME_SERIES = 'RenameSeries';
|
||||
export const RENAME_MOVIE = 'RenameMovie';
|
||||
export const RESET_API_KEY = 'ResetApiKey';
|
||||
export const RSS_SYNC = 'RssSync';
|
||||
export const MOVIE_SEARCH = 'MoviesSearch';
|
||||
|
@ -7,7 +7,7 @@ import styles from './MonitorToggleButton.css';
|
||||
|
||||
function getTooltip(monitored, isDisabled) {
|
||||
if (isDisabled) {
|
||||
return 'Cannot toogle monitored state when series is unmonitored';
|
||||
return 'Cannot toogle monitored state when movie is unmonitored';
|
||||
}
|
||||
|
||||
if (monitored) {
|
||||
|
@ -136,8 +136,8 @@ class MovieSearchInput extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
// If an suggestion is not selected go to the first series,
|
||||
// otherwise go to the selected series.
|
||||
// If an suggestion is not selected go to the first movie,
|
||||
// otherwise go to the selected movie.
|
||||
|
||||
if (highlightedSuggestionIndex == null) {
|
||||
this.goToMovie(suggestions[0]);
|
||||
|
@ -19,7 +19,7 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
|
||||
|
||||
const links = [
|
||||
{
|
||||
iconName: icons.SERIES_CONTINUING,
|
||||
iconName: icons.MOVIE_CONTINUING,
|
||||
title: 'Movies',
|
||||
to: '/',
|
||||
alias: '/movies',
|
||||
|
@ -34,6 +34,7 @@ import {
|
||||
faCalendarAlt as fasCalendarAlt,
|
||||
faCaretDown as fasCaretDown,
|
||||
faCheck as fasCheck,
|
||||
faCheckSquare as fasCheckSquare,
|
||||
faChevronCircleDown as fasChevronCircleDown,
|
||||
faChevronCircleRight as fasChevronCircleRight,
|
||||
faChevronCircleUp as fasChevronCircleUp,
|
||||
@ -117,6 +118,7 @@ export const CARET_DOWN = fasCaretDown;
|
||||
export const CHECK = fasCheck;
|
||||
export const CHECK_INDETERMINATE = fasMinus;
|
||||
export const CHECK_CIRCLE = fasCheckCircle;
|
||||
export const CHECK_SQUARE = fasCheckSquare;
|
||||
export const CIRCLE = fasCircle;
|
||||
export const CIRCLE_OUTLINE = farCircle;
|
||||
export const CLEAR = fasTrashAlt;
|
||||
@ -180,7 +182,7 @@ export const SAVE = fasSave;
|
||||
export const SCHEDULED = farClock;
|
||||
export const SCORE = fasUserPlus;
|
||||
export const SEARCH = fasSearch;
|
||||
export const SERIES_CONTINUING = fasPlay;
|
||||
export const MOVIE_CONTINUING = fasPlay;
|
||||
export const SERIES_ENDED = fasStop;
|
||||
export const SETTINGS = fasCogs;
|
||||
export const SHUTDOWN = fasPowerOff;
|
||||
|
@ -527,7 +527,7 @@ class MovieDetails extends Component {
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
folder={path}
|
||||
allowSeriesChange={false}
|
||||
allowMovieChange={false}
|
||||
showFilterExistingFiles={true}
|
||||
showImportMode={false}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
|
@ -69,7 +69,7 @@ function createMapStateToProps() {
|
||||
const isRefreshing = isMovieRefreshing || allMoviesRefreshing;
|
||||
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.MOVIE_SEARCH, movieIds: [movie.id] }));
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, movieId: movie.id }));
|
||||
const isRenamingMovieCommand = findCommand(commands, { name: commandNames.RENAME_SERIES });
|
||||
const isRenamingMovieCommand = findCommand(commands, { name: commandNames.RENAME_MOVIE });
|
||||
const isRenamingMovie = (
|
||||
isCommandExecuting(isRenamingMovieCommand) &&
|
||||
isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1
|
||||
|
@ -54,7 +54,7 @@ class MovieDetailsPageConnector extends Component {
|
||||
if (!titleSlug) {
|
||||
return (
|
||||
<NotFound
|
||||
message="Sorry, that series cannot be found."
|
||||
message="Sorry, that movie cannot be found."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ 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 MoveMovieModal from 'Movie/MoveSeries/MoveSeriesModal';
|
||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||
import styles from './EditMovieModalContent.css';
|
||||
|
||||
class EditMovieModalContent extends Component {
|
||||
@ -45,7 +45,7 @@ class EditMovieModalContent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onMoveSeriesPress = () => {
|
||||
onMoveMoviePress = () => {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
|
||||
this.props.onSavePress(true);
|
||||
@ -159,7 +159,7 @@ class EditMovieModalContent extends Component {
|
||||
destinationPath={path.value}
|
||||
isOpen={this.state.isConfirmMoveModalOpen}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveSeriesPress={this.onMoveSeriesPress}
|
||||
onMoveMoviePress={this.onMoveMoviePress}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
|
@ -29,21 +29,21 @@ function createMapStateToProps() {
|
||||
(state) => state.movies,
|
||||
createMovieSelector(),
|
||||
createIsPathChangingSelector(),
|
||||
(seriesState, movie, isPathChanging) => {
|
||||
(moviesState, movie, isPathChanging) => {
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges
|
||||
} = seriesState;
|
||||
} = moviesState;
|
||||
|
||||
const seriesSettings = _.pick(movie, [
|
||||
const movieSettings = _.pick(movie, [
|
||||
'monitored',
|
||||
'qualityProfileId',
|
||||
'path',
|
||||
'tags'
|
||||
]);
|
||||
|
||||
const settings = selectSettings(seriesSettings, pendingChanges, saveError);
|
||||
const settings = selectSettings(movieSettings, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
title: movie.title,
|
||||
@ -97,7 +97,7 @@ class EditMovieModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveSeriesPress={this.onMoveSeriesPress}
|
||||
onMoveMoviePress={this.onMoveMoviePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class DeleteMovieModalContent extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
series,
|
||||
movies,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
@ -51,19 +51,19 @@ class DeleteMovieModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Delete Selected Series
|
||||
Delete Selected Movie(s)
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{`Delete Series Folder${series.length > 1 ? 's' : ''}`}</FormLabel>
|
||||
<FormLabel>{`Delete Movie Folder${movies.length > 1 ? 's' : ''}`}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="deleteFiles"
|
||||
value={deleteFiles}
|
||||
helpText={`Delete Series Folder${series.length > 1 ? 's' : ''} and all contents`}
|
||||
helpText={`Delete Movie Folder${movies.length > 1 ? 's' : ''} and all contents`}
|
||||
kind={kinds.DANGER}
|
||||
onChange={this.onDeleteFilesChange}
|
||||
/>
|
||||
@ -71,12 +71,12 @@ class DeleteMovieModalContent extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.message}>
|
||||
{`Are you sure you want to delete ${series.length} selected series${deleteFiles ? ' and all contents' : ''}?`}
|
||||
{`Are you sure you want to delete ${movies.length} selected movie(s)${deleteFiles ? ' and all contents' : ''}?`}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{
|
||||
series.map((s) => {
|
||||
movies.map((s) => {
|
||||
return (
|
||||
<li key={s.title}>
|
||||
<span>{s.title}</span>
|
||||
@ -115,7 +115,7 @@ class DeleteMovieModalContent extends Component {
|
||||
}
|
||||
|
||||
DeleteMovieModalContent.propTypes = {
|
||||
series: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteSelectedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
@ -2,20 +2,20 @@ import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import { bulkDeleteMovie } from 'Store/Actions/movieEditorActions';
|
||||
import { bulkDeleteMovie } from 'Store/Actions/movieIndexActions';
|
||||
import DeleteMovieModalContent from './DeleteMovieModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { seriesIds }) => seriesIds,
|
||||
(state, { movieIds }) => movieIds,
|
||||
createAllMoviesSelector(),
|
||||
(seriesIds, allMovies) => {
|
||||
const selectedMovie = _.intersectionWith(allMovies, seriesIds, (s, id) => {
|
||||
(movieIds, allMovies) => {
|
||||
const selectedMovie = _.intersectionWith(allMovies, movieIds, (s, id) => {
|
||||
return s.id === id;
|
||||
});
|
||||
|
||||
const sortedSeries = _.orderBy(selectedMovie, 'sortTitle');
|
||||
const series = _.map(sortedSeries, (s) => {
|
||||
const sortedMovies = _.orderBy(selectedMovie, 'sortTitle');
|
||||
const movies = _.map(sortedMovies, (s) => {
|
||||
return {
|
||||
title: s.title,
|
||||
path: s.path
|
||||
@ -23,7 +23,7 @@ function createMapStateToProps() {
|
||||
});
|
||||
|
||||
return {
|
||||
series
|
||||
movies
|
||||
};
|
||||
}
|
||||
);
|
||||
@ -33,7 +33,7 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onDeleteSelectedPress(deleteFiles) {
|
||||
dispatch(bulkDeleteMovie({
|
||||
seriesIds: props.seriesIds,
|
||||
movieIds: props.movieIds,
|
||||
deleteFiles
|
||||
}));
|
||||
|
||||
|
@ -6,15 +6,15 @@ import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSe
|
||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import MoveSeriesModal from 'Movie/MoveSeries/MoveSeriesModal';
|
||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||
import TagsModal from './Tags/TagsModal';
|
||||
import DeleteMovieModal from './Delete/DeleteMovieModal';
|
||||
import SeriesEditorFooterLabel from './SeriesEditorFooterLabel';
|
||||
import styles from './SeriesEditorFooter.css';
|
||||
import MovieEditorFooterLabel from './MovieEditorFooterLabel';
|
||||
import styles from './MovieEditorFooter.css';
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
class SeriesEditorFooter extends Component {
|
||||
class MovieEditorFooter extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@ -112,7 +112,7 @@ class SeriesEditorFooter extends Component {
|
||||
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
|
||||
}
|
||||
|
||||
onMoveSeriesPress = () => {
|
||||
onMoveMoviePress = () => {
|
||||
this.setState({
|
||||
isConfirmMoveModalOpen: false,
|
||||
destinationRootFolder: null
|
||||
@ -129,12 +129,12 @@ class SeriesEditorFooter extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
seriesIds,
|
||||
movieIds,
|
||||
selectedCount,
|
||||
isSaving,
|
||||
isDeleting,
|
||||
isOrganizingSeries,
|
||||
onOrganizeSeriesPress
|
||||
isOrganizingMovie,
|
||||
onOrganizeMoviePress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -157,8 +157,8 @@ class SeriesEditorFooter extends Component {
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<SeriesEditorFooterLabel
|
||||
label="Monitor Series"
|
||||
<MovieEditorFooterLabel
|
||||
label="Monitor Movie"
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
@ -172,7 +172,7 @@ class SeriesEditorFooter extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<SeriesEditorFooterLabel
|
||||
<MovieEditorFooterLabel
|
||||
label="Quality Profile"
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
@ -187,7 +187,7 @@ class SeriesEditorFooter extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<SeriesEditorFooterLabel
|
||||
<MovieEditorFooterLabel
|
||||
label="Root Folder"
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
@ -204,8 +204,8 @@ class SeriesEditorFooter extends Component {
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<SeriesEditorFooterLabel
|
||||
label={`${selectedCount} Series Selected`}
|
||||
<MovieEditorFooterLabel
|
||||
label={`${selectedCount} Movie(s) Selected`}
|
||||
isSaving={false}
|
||||
/>
|
||||
|
||||
@ -214,9 +214,9 @@ class SeriesEditorFooter extends Component {
|
||||
<SpinnerButton
|
||||
className={styles.organizeSelectedButton}
|
||||
kind={kinds.WARNING}
|
||||
isSpinning={isOrganizingSeries}
|
||||
isDisabled={!selectedCount || isOrganizingSeries}
|
||||
onPress={onOrganizeSeriesPress}
|
||||
isSpinning={isOrganizingMovie}
|
||||
isDisabled={!selectedCount || isOrganizingMovie}
|
||||
onPress={onOrganizeMoviePress}
|
||||
>
|
||||
Rename Files
|
||||
</SpinnerButton>
|
||||
@ -224,7 +224,7 @@ class SeriesEditorFooter extends Component {
|
||||
<SpinnerButton
|
||||
className={styles.tagsButton}
|
||||
isSpinning={isSaving && savingTags}
|
||||
isDisabled={!selectedCount || isOrganizingSeries}
|
||||
isDisabled={!selectedCount || isOrganizingMovie}
|
||||
onPress={this.onTagsPress}
|
||||
>
|
||||
Set Tags
|
||||
@ -246,38 +246,38 @@ class SeriesEditorFooter extends Component {
|
||||
|
||||
<TagsModal
|
||||
isOpen={isTagsModalOpen}
|
||||
seriesIds={seriesIds}
|
||||
movieIds={movieIds}
|
||||
onApplyTagsPress={this.onApplyTagsPress}
|
||||
onModalClose={this.onTagsModalClose}
|
||||
/>
|
||||
|
||||
<DeleteMovieModal
|
||||
isOpen={isDeleteMovieModalOpen}
|
||||
seriesIds={seriesIds}
|
||||
movieIds={movieIds}
|
||||
onModalClose={this.onDeleteMovieModalClose}
|
||||
/>
|
||||
|
||||
<MoveSeriesModal
|
||||
<MoveMovieModal
|
||||
destinationRootFolder={destinationRootFolder}
|
||||
isOpen={isConfirmMoveModalOpen}
|
||||
onSavePress={this.onSaveRootFolderPress}
|
||||
onMoveSeriesPress={this.onMoveSeriesPress}
|
||||
onMoveMoviePress={this.onMoveMoviePress}
|
||||
/>
|
||||
</PageContentFooter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SeriesEditorFooter.propTypes = {
|
||||
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
MovieEditorFooter.propTypes = {
|
||||
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingSeries: PropTypes.bool.isRequired,
|
||||
isOrganizingMovie: PropTypes.bool.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired,
|
||||
onOrganizeSeriesPress: PropTypes.func.isRequired
|
||||
onOrganizeMoviePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SeriesEditorFooter;
|
||||
export default MovieEditorFooter;
|
@ -2,9 +2,9 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||
import styles from './SeriesEditorFooterLabel.css';
|
||||
import styles from './MovieEditorFooterLabel.css';
|
||||
|
||||
function SeriesEditorFooterLabel(props) {
|
||||
function MovieEditorFooterLabel(props) {
|
||||
const {
|
||||
className,
|
||||
label,
|
||||
@ -27,14 +27,14 @@ function SeriesEditorFooterLabel(props) {
|
||||
);
|
||||
}
|
||||
|
||||
SeriesEditorFooterLabel.propTypes = {
|
||||
MovieEditorFooterLabel.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
SeriesEditorFooterLabel.defaultProps = {
|
||||
MovieEditorFooterLabel.defaultProps = {
|
||||
className: styles.label
|
||||
};
|
||||
|
||||
export default SeriesEditorFooterLabel;
|
||||
export default MovieEditorFooterLabel;
|
@ -1,9 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import OrganizeSeriesModalContentConnector from './OrganizeSeriesModalContentConnector';
|
||||
import OrganizeMovieModalContentConnector from './OrganizeMovieModalContentConnector';
|
||||
|
||||
function OrganizeSeriesModal(props) {
|
||||
function OrganizeMovieModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
@ -15,7 +15,7 @@ function OrganizeSeriesModal(props) {
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<OrganizeSeriesModalContentConnector
|
||||
<OrganizeMovieModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
@ -23,9 +23,9 @@ function OrganizeSeriesModal(props) {
|
||||
);
|
||||
}
|
||||
|
||||
OrganizeSeriesModal.propTypes = {
|
||||
OrganizeMovieModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default OrganizeSeriesModal;
|
||||
export default OrganizeMovieModal;
|
@ -8,24 +8,24 @@ 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 styles from './OrganizeSeriesModalContent.css';
|
||||
import styles from './OrganizeMovieModalContent.css';
|
||||
|
||||
function OrganizeSeriesModalContent(props) {
|
||||
function OrganizeMovieModalContent(props) {
|
||||
const {
|
||||
seriesTitles,
|
||||
movieTitles,
|
||||
onModalClose,
|
||||
onOrganizeSeriesPress
|
||||
onOrganizeMoviePress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Organize Selected Series
|
||||
Organize Selected Movies
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert>
|
||||
Tip: To preview a rename... select "Cancel" then any series title and use the
|
||||
Tip: To preview a rename... select "Cancel" then click any movie title and use the
|
||||
<Icon
|
||||
className={styles.renameIcon}
|
||||
name={icons.ORGANIZE}
|
||||
@ -33,12 +33,12 @@ function OrganizeSeriesModalContent(props) {
|
||||
</Alert>
|
||||
|
||||
<div className={styles.message}>
|
||||
Are you sure you want to organize all files in the {seriesTitles.length} selected series?
|
||||
Are you sure you want to organize all files in the {movieTitles.length} selected movie(s)?
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{
|
||||
seriesTitles.map((title) => {
|
||||
movieTitles.map((title) => {
|
||||
return (
|
||||
<li key={title}>
|
||||
{title}
|
||||
@ -56,7 +56,7 @@ function OrganizeSeriesModalContent(props) {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={onOrganizeSeriesPress}
|
||||
onPress={onOrganizeMoviePress}
|
||||
>
|
||||
Organize
|
||||
</Button>
|
||||
@ -65,10 +65,10 @@ function OrganizeSeriesModalContent(props) {
|
||||
);
|
||||
}
|
||||
|
||||
OrganizeSeriesModalContent.propTypes = {
|
||||
seriesTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
OrganizeMovieModalContent.propTypes = {
|
||||
movieTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onOrganizeSeriesPress: PropTypes.func.isRequired
|
||||
onOrganizeMoviePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default OrganizeSeriesModalContent;
|
||||
export default OrganizeMovieModalContent;
|
@ -6,22 +6,22 @@ import { createSelector } from 'reselect';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import OrganizeSeriesModalContent from './OrganizeSeriesModalContent';
|
||||
import OrganizeMovieModalContent from './OrganizeMovieModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { seriesIds }) => seriesIds,
|
||||
(state, { movieIds }) => movieIds,
|
||||
createAllMoviesSelector(),
|
||||
(seriesIds, allMovies) => {
|
||||
const series = _.intersectionWith(allMovies, seriesIds, (s, id) => {
|
||||
(movieIds, allMovies) => {
|
||||
const movies = _.intersectionWith(allMovies, movieIds, (s, id) => {
|
||||
return s.id === id;
|
||||
});
|
||||
|
||||
const sortedSeries = _.orderBy(series, 'sortTitle');
|
||||
const seriesTitles = _.map(sortedSeries, 'title');
|
||||
const sortedMovies = _.orderBy(movies, 'sortTitle');
|
||||
const movieTitles = _.map(sortedMovies, 'title');
|
||||
|
||||
return {
|
||||
seriesTitles
|
||||
movieTitles
|
||||
};
|
||||
}
|
||||
);
|
||||
@ -31,15 +31,15 @@ const mapDispatchToProps = {
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class OrganizeSeriesModalContentConnector extends Component {
|
||||
class OrganizeMovieModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOrganizeSeriesPress = () => {
|
||||
onOrganizeMoviePress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.RENAME_SERIES,
|
||||
seriesIds: this.props.seriesIds
|
||||
name: commandNames.RENAME_MOVIE,
|
||||
movieIds: this.props.movieIds
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
@ -50,18 +50,18 @@ class OrganizeSeriesModalContentConnector extends Component {
|
||||
|
||||
render(props) {
|
||||
return (
|
||||
<OrganizeSeriesModalContent
|
||||
<OrganizeMovieModalContent
|
||||
{...this.props}
|
||||
onOrganizeSeriesPress={this.onOrganizeSeriesPress}
|
||||
onOrganizeMoviePress={this.onOrganizeMoviePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrganizeSeriesModalContentConnector.propTypes = {
|
||||
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
OrganizeMovieModalContentConnector.propTypes = {
|
||||
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeSeriesModalContentConnector);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeMovieModalContentConnector);
|
@ -1,268 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import NoMovie from 'Movie/NoMovie';
|
||||
import OrganizeSeriesModal from './Organize/OrganizeSeriesModal';
|
||||
import SeriesEditorRowConnector from './SeriesEditorRowConnector';
|
||||
import SeriesEditorFooter from './SeriesEditorFooter';
|
||||
import SeriesEditorFilterModalConnector from './SeriesEditorFilterModalConnector';
|
||||
|
||||
function getColumns() {
|
||||
return [
|
||||
{
|
||||
name: 'status',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sortTitle',
|
||||
label: 'Title',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
label: 'Quality Profile',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
class SeriesEditor extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isOrganizingSeriesModalOpen: false,
|
||||
columns: getColumns()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = this.props;
|
||||
|
||||
const hasFinishedDeleting = prevProps.isDeleting &&
|
||||
!isDeleting &&
|
||||
!deleteError;
|
||||
|
||||
if (hasFinishedDeleting) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSelected = (changes) => {
|
||||
this.props.onSaveSelected({
|
||||
seriesIds: this.getSelectedIds(),
|
||||
...changes
|
||||
});
|
||||
}
|
||||
|
||||
onOrganizeSeriesPress = () => {
|
||||
this.setState({ isOrganizingSeriesModalOpen: true });
|
||||
}
|
||||
|
||||
onOrganizeSeriesModalClose = (organized) => {
|
||||
this.setState({ isOrganizingSeriesModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalItems,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
isOrganizingSeries,
|
||||
onSortPress,
|
||||
onFilterSelect
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
columns
|
||||
} = this.state;
|
||||
|
||||
const selectedMovieIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title="Series Editor">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection />
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={SeriesEditorFilterModalConnector}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load the calendar</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSortPress={onSortPress}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<SeriesEditorRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
columns={columns}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoMovie totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
|
||||
<SeriesEditorFooter
|
||||
seriesIds={selectedMovieIds}
|
||||
selectedCount={selectedMovieIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
isOrganizingSeries={isOrganizingSeries}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeSeriesPress={this.onOrganizeSeriesPress}
|
||||
/>
|
||||
|
||||
<OrganizeSeriesModal
|
||||
isOpen={this.state.isOrganizingSeriesModalOpen}
|
||||
seriesIds={selectedMovieIds}
|
||||
onModalClose={this.onOrganizeSeriesModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SeriesEditor.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingSeries: PropTypes.bool.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SeriesEditor;
|
@ -1,88 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { setSeriesEditorSort, setSeriesEditorFilter, saveSeriesEditor } from 'Store/Actions/movieEditorActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import SeriesEditor from './SeriesEditor';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('movies', 'movieEditor'),
|
||||
createCommandExecutingSelector(commandNames.RENAME_SERIES),
|
||||
(series, isOrganizingSeries) => {
|
||||
return {
|
||||
isOrganizingSeries,
|
||||
...series
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetSeriesEditorSort: setSeriesEditorSort,
|
||||
dispatchSetSeriesEditorFilter: setSeriesEditorFilter,
|
||||
dispatchSaveMovieEditor: saveSeriesEditor,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class SeriesEditorConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.dispatchSetSeriesEditorSort({ sortKey });
|
||||
}
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.dispatchSetSeriesEditorFilter({ selectedFilterKey });
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveMovieEditor(payload);
|
||||
}
|
||||
|
||||
onMoveSelected = (payload) => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.MOVE_SERIES,
|
||||
...payload
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SeriesEditor
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SeriesEditorConnector.propTypes = {
|
||||
dispatchSetSeriesEditorSort: PropTypes.func.isRequired,
|
||||
dispatchSetSeriesEditorFilter: PropTypes.func.isRequired,
|
||||
dispatchSaveMovieEditor: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SeriesEditorConnector);
|
@ -1,24 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setSeriesEditorFilter } from 'Store/Actions/movieEditorActions';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movies.items,
|
||||
(state) => state.moviesEditor.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'movieEditor'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setSeriesEditorFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
@ -1,97 +0,0 @@
|
||||
// import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
// import titleCase from 'Utilities/String/titleCase';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
// import CheckInput from 'Components/Form/CheckInput';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import MovieStatusCell from 'Movie/Index/Table/MovieStatusCell';
|
||||
|
||||
class SeriesEditorRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSeasonFolderChange = () => {
|
||||
// Mock handler to satisfy `onChange` being required for `CheckInput`.
|
||||
//
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
status,
|
||||
titleSlug,
|
||||
title,
|
||||
monitored,
|
||||
qualityProfile,
|
||||
path,
|
||||
tags,
|
||||
// columns,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<MovieStatusCell
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
/>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieTitleLink
|
||||
titleSlug={titleSlug}
|
||||
title={title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{qualityProfile.name}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{path}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<TagListConnector
|
||||
tags={tags}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SeriesEditorRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
SeriesEditorRow.defaultProps = {
|
||||
tags: []
|
||||
};
|
||||
|
||||
export default SeriesEditorRow;
|
@ -1,31 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
|
||||
import SeriesEditorRow from './SeriesEditorRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createQualityProfileSelector(),
|
||||
(qualityProfile) => {
|
||||
return {
|
||||
qualityProfile
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function SeriesEditorRowConnector(props) {
|
||||
return (
|
||||
<SeriesEditorRow
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SeriesEditorRowConnector.propTypes = {
|
||||
qualityProfileId: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(SeriesEditorRowConnector);
|
@ -49,7 +49,7 @@ class TagsModalContent extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
seriesTags,
|
||||
movieTags,
|
||||
tagList,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@ -93,7 +93,7 @@ class TagsModalContent extends Component {
|
||||
value={applyTags}
|
||||
values={applyTagsOptions}
|
||||
helpTexts={[
|
||||
'How to apply tags to the selected series',
|
||||
'How to apply tags to the selected movies',
|
||||
'Add: Add the tags the existing list of tags',
|
||||
'Remove: Remove the entered tags',
|
||||
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)'
|
||||
@ -107,7 +107,7 @@ class TagsModalContent extends Component {
|
||||
|
||||
<div className={styles.result}>
|
||||
{
|
||||
seriesTags.map((t) => {
|
||||
movieTags.map((t) => {
|
||||
const tag = _.find(tagList, { id: t });
|
||||
|
||||
if (!tag) {
|
||||
@ -139,7 +139,7 @@ class TagsModalContent extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (seriesTags.indexOf(t) > -1) {
|
||||
if (movieTags.indexOf(t) > -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -178,7 +178,7 @@ class TagsModalContent extends Component {
|
||||
}
|
||||
|
||||
TagsModalContent.propTypes = {
|
||||
seriesTags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
movieTags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onApplyTagsPress: PropTypes.func.isRequired
|
||||
|
@ -7,18 +7,18 @@ import TagsModalContent from './TagsModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { seriesIds }) => seriesIds,
|
||||
(state, { movieIds }) => movieIds,
|
||||
createAllMoviesSelector(),
|
||||
createTagsSelector(),
|
||||
(seriesIds, allMovies, tagList) => {
|
||||
const series = _.intersectionWith(allMovies, seriesIds, (s, id) => {
|
||||
(movieIds, allMovies, tagList) => {
|
||||
const movies = _.intersectionWith(allMovies, movieIds, (s, id) => {
|
||||
return s.id === id;
|
||||
});
|
||||
|
||||
const seriesTags = _.uniq(_.concat(..._.map(series, 'tags')));
|
||||
const movieTags = _.uniq(_.concat(..._.map(movies, 'tags')));
|
||||
|
||||
return {
|
||||
seriesTags,
|
||||
movieTags,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
.blankpad {
|
||||
padding-left:2em;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ 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 selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@ -23,7 +26,9 @@ import MovieIndexFilterMenu from './Menus/MovieIndexFilterMenu';
|
||||
import MovieIndexSortMenu from './Menus/MovieIndexSortMenu';
|
||||
import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
|
||||
import MovieIndexFooterConnector from './MovieIndexFooterConnector';
|
||||
import MovieEditorFooter from 'Movie/Editor/MovieEditorFooter.js';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import OrganizeMovieModal from 'Movie/Editor/Organize/OrganizeMovieModal';
|
||||
import styles from './MovieIndex.css';
|
||||
|
||||
function getViewComponent(view) {
|
||||
@ -53,12 +58,19 @@ class MovieIndex extends Component {
|
||||
isPosterOptionsModalOpen: false,
|
||||
isOverviewOptionsModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isMovieEditorActive: false,
|
||||
isOrganizingMovieModalOpen: false,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isRendered: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@ -66,7 +78,9 @@ class MovieIndex extends Component {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
scrollTop
|
||||
scrollTop,
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
@ -75,11 +89,20 @@ class MovieIndex extends Component {
|
||||
sortDirection !== prevProps.sortDirection
|
||||
) {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
if (this.state.jumpToCharacter != null && scrollTop !== prevProps.scrollTop) {
|
||||
this.setState({ jumpToCharacter: null });
|
||||
}
|
||||
|
||||
const hasFinishedDeleting = prevProps.isDeleting &&
|
||||
!isDeleting &&
|
||||
!deleteError;
|
||||
|
||||
if (hasFinishedDeleting) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@ -89,6 +112,45 @@ class MovieIndex extends Component {
|
||||
this.setState({ contentBody: ref });
|
||||
}
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
setSelectedState() {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((movie) => {
|
||||
const isItemSelected = selectedState[movie.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[movie.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[movie.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
}
|
||||
|
||||
setJumpBarItems() {
|
||||
const {
|
||||
items,
|
||||
@ -149,10 +211,51 @@ class MovieIndex extends Component {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
|
||||
onMovieEditorTogglePress = () => {
|
||||
if (this.state.isMovieEditorActive) {
|
||||
this.setState({ isMovieEditorActive: false });
|
||||
} else {
|
||||
this.setState({ isMovieEditorActive: true });
|
||||
}
|
||||
}
|
||||
|
||||
onJumpBarItemPress = (jumpToCharacter) => {
|
||||
this.setState({ jumpToCharacter });
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectAllPress = () => {
|
||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSelected = (changes) => {
|
||||
this.props.onSaveSelected({
|
||||
movieIds: this.getSelectedIds(),
|
||||
...changes
|
||||
});
|
||||
}
|
||||
|
||||
onOrganizeMoviePress = () => {
|
||||
this.setState({ isOrganizingMovieModalOpen: true });
|
||||
}
|
||||
|
||||
onOrganizeMovieModalClose = (organized) => {
|
||||
this.setState({ isOrganizingMovieModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
onRender = () => {
|
||||
this.setState({ isRendered: true }, () => {
|
||||
const {
|
||||
@ -193,6 +296,11 @@ class MovieIndex extends Component {
|
||||
view,
|
||||
isRefreshingMovie,
|
||||
isRssSyncExecuting,
|
||||
isOrganizingMovie,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
scrollTop,
|
||||
onSortSelect,
|
||||
onFilterSelect,
|
||||
@ -209,9 +317,15 @@ class MovieIndex extends Component {
|
||||
isPosterOptionsModalOpen,
|
||||
isOverviewOptionsModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isRendered
|
||||
isMovieEditorActive,
|
||||
isRendered,
|
||||
selectedState,
|
||||
allSelected,
|
||||
allUnselected
|
||||
} = this.state;
|
||||
|
||||
const selectedMovieIds = this.getSelectedIds();
|
||||
|
||||
const ViewComponent = getViewComponent(view);
|
||||
const isLoaded = !!(!error && isPopulated && items.length && contentBody);
|
||||
const hasNoMovie = !totalItems;
|
||||
@ -248,16 +362,38 @@ class MovieIndex extends Component {
|
||||
<PageToolbarButton
|
||||
label="Manual Import"
|
||||
iconName={icons.INTERACTIVE}
|
||||
isDisabled={hasNoMovie}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Movie Editor"
|
||||
iconName={icons.EDIT}
|
||||
isDisabled={hasNoMovie}
|
||||
/>
|
||||
{
|
||||
isMovieEditorActive ?
|
||||
<PageToolbarButton
|
||||
label="Movie Index"
|
||||
iconName={icons.MOVIE_CONTINUING}
|
||||
isDisabled={hasNoMovie}
|
||||
onPress={this.onMovieEditorTogglePress}
|
||||
/> :
|
||||
<PageToolbarButton
|
||||
label="Movie Editor"
|
||||
iconName={icons.EDIT}
|
||||
isDisabled={hasNoMovie}
|
||||
onPress={this.onMovieEditorTogglePress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isMovieEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={allSelected ? 'Unselect All' : 'Select All'}
|
||||
iconName={icons.CHECK_SQUARE}
|
||||
isDisabled={hasNoMovie}
|
||||
onPress={this.onSelectAllPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
@ -360,10 +496,19 @@ class MovieIndex extends Component {
|
||||
scrollTop={scrollTop}
|
||||
jumpToCharacter={jumpToCharacter}
|
||||
onRender={this.onRender}
|
||||
isMovieEditorActive={isMovieEditorActive}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
selectedState={selectedState}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<MovieIndexFooterConnector />
|
||||
{
|
||||
!isMovieEditorActive &&
|
||||
<MovieIndexFooterConnector />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -382,6 +527,21 @@ class MovieIndex extends Component {
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
isLoaded && isMovieEditorActive &&
|
||||
<MovieEditorFooter
|
||||
movieIds={selectedMovieIds}
|
||||
selectedCount={selectedMovieIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
isOrganizingMovie={isOrganizingMovie}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeMoviePress={this.onOrganizeMoviePress}
|
||||
/>
|
||||
}
|
||||
|
||||
<MovieIndexPosterOptionsModal
|
||||
isOpen={isPosterOptionsModalOpen}
|
||||
onModalClose={this.onPosterOptionsModalClose}
|
||||
@ -396,6 +556,12 @@ class MovieIndex extends Component {
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<OrganizeMovieModal
|
||||
isOpen={this.state.isOrganizingMovieModalOpen}
|
||||
movieIds={selectedMovieIds}
|
||||
onModalClose={this.onOrganizeMovieModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@ -415,15 +581,21 @@ MovieIndex.propTypes = {
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
view: PropTypes.string.isRequired,
|
||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
||||
isOrganizingMovie: PropTypes.bool.isRequired,
|
||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onViewSelect: PropTypes.func.isRequired,
|
||||
onRefreshMoviePress: PropTypes.func.isRequired,
|
||||
onRssSyncPress: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieIndex;
|
||||
|
@ -8,7 +8,7 @@ import createCommandExecutingSelector from 'Store/Selectors/createCommandExecuti
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import { fetchMovies } from 'Store/Actions/movieActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import { setMovieSort, setMovieFilter, setMovieView, setMovieTableOption } from 'Store/Actions/movieIndexActions';
|
||||
import { setMovieSort, setMovieFilter, setMovieView, setMovieTableOption, saveMovieEditor } from 'Store/Actions/movieIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
@ -42,17 +42,20 @@ function createMapStateToProps() {
|
||||
createMovieClientSideCollectionItemsSelector('movieIndex'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MOVIE),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createCommandExecutingSelector(commandNames.RENAME_MOVIE),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
movies,
|
||||
isRefreshingMovie,
|
||||
isRssSyncExecuting,
|
||||
isOrganizingMovie,
|
||||
dimensionsState
|
||||
) => {
|
||||
return {
|
||||
...movies,
|
||||
isRefreshingMovie,
|
||||
isRssSyncExecuting,
|
||||
isOrganizingMovie,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
}
|
||||
@ -81,6 +84,10 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatch(setMovieView({ view }));
|
||||
},
|
||||
|
||||
dispatchSaveMovieEditor(payload) {
|
||||
dispatch(saveMovieEditor(payload));
|
||||
},
|
||||
|
||||
onRefreshMoviePress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.REFRESH_MOVIE
|
||||
@ -128,6 +135,10 @@ class MovieIndexConnector extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveMovieEditor(payload);
|
||||
}
|
||||
|
||||
onScroll = ({ scrollTop }) => {
|
||||
this.setState({
|
||||
scrollTop
|
||||
@ -146,6 +157,7 @@ class MovieIndexConnector extends Component {
|
||||
scrollTop={this.state.scrollTop}
|
||||
onViewSelect={this.onViewSelect}
|
||||
onScroll={this.onScroll}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -156,7 +168,8 @@ MovieIndexConnector.propTypes = {
|
||||
view: PropTypes.string.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
dispatchFetchMovies: PropTypes.func.isRequired,
|
||||
dispatchSetMovieView: PropTypes.func.isRequired
|
||||
dispatchSetMovieView: PropTypes.func.isRequired,
|
||||
dispatchSaveMovieEditor: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withScrollPosition(
|
||||
|
@ -17,6 +17,13 @@ $hoverScale: 1.05;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editorSelect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.posterContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import fonts from 'Styles/Variables/fonts';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
@ -65,6 +66,15 @@ class MovieIndexOverview extends Component {
|
||||
this.setState({ isDeleteMovieModalOpen: false });
|
||||
}
|
||||
|
||||
onChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@ -94,6 +104,9 @@ class MovieIndexOverview extends Component {
|
||||
isSearchingMovie,
|
||||
onRefreshMoviePress,
|
||||
onSearchPress,
|
||||
isMovieEditorActive,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@ -118,11 +131,15 @@ class MovieIndexOverview extends Component {
|
||||
<div className={styles.poster}>
|
||||
<div className={styles.posterContainer}>
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
className={styles.ended}
|
||||
title="Ended"
|
||||
/>
|
||||
isMovieEditorActive &&
|
||||
<div className={styles.editorSelect}>
|
||||
<CheckInput
|
||||
className={styles.checkInput}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Link
|
||||
@ -253,7 +270,10 @@ MovieIndexOverview.propTypes = {
|
||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
||||
isSearchingMovie: PropTypes.bool.isRequired,
|
||||
onRefreshMoviePress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isMovieEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieIndexOverview;
|
||||
|
@ -169,7 +169,10 @@ class MovieIndexOverviews extends Component {
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
isSmallScreen
|
||||
isSmallScreen,
|
||||
selectedState,
|
||||
isMovieEditorActive,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -201,6 +204,9 @@ class MovieIndexOverviews extends Component {
|
||||
style={style}
|
||||
movieId={movie.id}
|
||||
qualityProfileId={movie.qualityProfileId}
|
||||
isSelected={selectedState[movie.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isMovieEditorActive={isMovieEditorActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -227,7 +233,8 @@ class MovieIndexOverviews extends Component {
|
||||
items,
|
||||
scrollTop,
|
||||
isSmallScreen,
|
||||
onScroll
|
||||
onScroll,
|
||||
selectedState
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -257,6 +264,7 @@ class MovieIndexOverviews extends Component {
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
onSectionRendered={this.onSectionRendered}
|
||||
selectedState={selectedState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -282,7 +290,10 @@ MovieIndexOverviews.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onRender: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
isMovieEditorActive: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default MovieIndexOverviews;
|
||||
|
@ -85,6 +85,13 @@ $hoverScale: 1.05;
|
||||
transition: opacity 0;
|
||||
}
|
||||
|
||||
.editorSelect {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.action {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
@ -61,6 +62,15 @@ class MovieIndexPoster extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@ -89,6 +99,9 @@ class MovieIndexPoster extends Component {
|
||||
isSearchingMovie,
|
||||
onRefreshMoviePress,
|
||||
onSearchPress,
|
||||
isMovieEditorActive,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@ -109,6 +122,17 @@ class MovieIndexPoster extends Component {
|
||||
<div className={styles.container} style={style}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
{
|
||||
isMovieEditorActive &&
|
||||
<div className={styles.editorSelect}>
|
||||
<CheckInput
|
||||
className={styles.checkInput}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<Label className={styles.controls}>
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
@ -249,7 +273,10 @@ MovieIndexPoster.propTypes = {
|
||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
||||
isSearchingMovie: PropTypes.bool.isRequired,
|
||||
onRefreshMoviePress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isMovieEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
MovieIndexPoster.defaultProps = {
|
||||
|
@ -195,7 +195,10 @@ class MovieIndexPosters extends Component {
|
||||
posterOptions,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
timeFormat,
|
||||
selectedState,
|
||||
isMovieEditorActive,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -234,6 +237,9 @@ class MovieIndexPosters extends Component {
|
||||
style={style}
|
||||
movieId={movie.id}
|
||||
qualityProfileId={movie.qualityProfileId}
|
||||
isSelected={selectedState[movie.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isMovieEditorActive={isMovieEditorActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -260,7 +266,8 @@ class MovieIndexPosters extends Component {
|
||||
items,
|
||||
scrollTop,
|
||||
isSmallScreen,
|
||||
onScroll
|
||||
onScroll,
|
||||
selectedState
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -294,6 +301,7 @@ class MovieIndexPosters extends Component {
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
onSectionRendered={this.onSectionRendered}
|
||||
selectedState={selectedState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -318,7 +326,10 @@ MovieIndexPosters.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onRender: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
isMovieEditorActive: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default MovieIndexPosters;
|
||||
|
@ -144,7 +144,7 @@ class MovieIndexPosterOptionsModalContent extends Component {
|
||||
type={inputTypes.CHECK}
|
||||
name="showTitle"
|
||||
value={showTitle}
|
||||
helpText="Show series title under poster"
|
||||
helpText="Show movie title under poster"
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -4,6 +4,7 @@ import { icons } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
|
||||
import MovieIndexTableOptionsConnector from './MovieIndexTableOptionsConnector';
|
||||
import styles from './MovieIndexHeader.css';
|
||||
@ -39,6 +40,10 @@ class MovieIndexHeader extends Component {
|
||||
const {
|
||||
columns,
|
||||
onTableOptionChange,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
isMovieEditorActive,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@ -57,6 +62,17 @@ class MovieIndexHeader extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMovieEditorActive && name === 'select') {
|
||||
return (
|
||||
<VirtualTableSelectAllHeaderCell
|
||||
key={name}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
@ -102,7 +118,11 @@ class MovieIndexHeader extends Component {
|
||||
|
||||
MovieIndexHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
isMovieEditorActive: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default MovieIndexHeader;
|
||||
|
@ -15,6 +15,7 @@ import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieStatusCell from './MovieStatusCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import styles from './MovieIndexRow.css';
|
||||
|
||||
class MovieIndexRow extends Component {
|
||||
@ -80,8 +81,11 @@ class MovieIndexRow extends Component {
|
||||
columns,
|
||||
isRefreshingMovie,
|
||||
isSearchingMovie,
|
||||
isMovieEditorActive,
|
||||
isSelected,
|
||||
onRefreshMoviePress,
|
||||
onSearchPress
|
||||
onSearchPress,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -102,6 +106,19 @@ class MovieIndexRow extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMovieEditorActive && name === 'select') {
|
||||
return (
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.checkInput}
|
||||
id={id}
|
||||
key={name}
|
||||
isSelected={isSelected}
|
||||
isDisabled={false}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<MovieStatusCell
|
||||
@ -322,7 +339,10 @@ MovieIndexRow.propTypes = {
|
||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
||||
isSearchingMovie: PropTypes.bool.isRequired,
|
||||
onRefreshMoviePress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isMovieEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
MovieIndexRow.defaultProps = {
|
||||
|
@ -22,10 +22,13 @@ class MovieIndexTable extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const jumpToCharacter = this.props.jumpToCharacter;
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const items = this.props.items;
|
||||
|
||||
const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
@ -43,7 +46,10 @@ class MovieIndexTable extends Component {
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
columns
|
||||
columns,
|
||||
selectedState,
|
||||
onSelectedChange,
|
||||
isMovieEditorActive
|
||||
} = this.props;
|
||||
|
||||
const movie = items[rowIndex];
|
||||
@ -56,6 +62,9 @@ class MovieIndexTable extends Component {
|
||||
columns={columns}
|
||||
movieId={movie.id}
|
||||
qualityProfileId={movie.qualityProfileId}
|
||||
isSelected={selectedState[movie.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isMovieEditorActive={isMovieEditorActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -75,7 +84,12 @@ class MovieIndexTable extends Component {
|
||||
contentBody,
|
||||
onSortPress,
|
||||
onRender,
|
||||
onScroll
|
||||
onScroll,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
isMovieEditorActive,
|
||||
selectedState
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -95,8 +109,13 @@ class MovieIndexTable extends Component {
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
isMovieEditorActive={isMovieEditorActive}
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
columns={columns}
|
||||
filters={filters}
|
||||
sortKey={sortKey}
|
||||
@ -120,7 +139,13 @@ MovieIndexTable.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onRender: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
isMovieEditorActive: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default MovieIndexTable;
|
||||
|
@ -27,7 +27,7 @@ function MovieStatusCell(props) {
|
||||
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={status === 'released' ? icons.SERIES_ENDED : icons.SERIES_CONTINUING}
|
||||
name={status === 'released' ? icons.SERIES_ENDED : icons.MOVIE_CONTINUING}
|
||||
title={status === 'ended' ? 'Ended' : 'Continuing'}
|
||||
|
||||
/>
|
||||
|
@ -7,16 +7,16 @@ 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 styles from './MoveSeriesModal.css';
|
||||
import styles from './MoveMovieModal.css';
|
||||
|
||||
function MoveSeriesModal(props) {
|
||||
function MoveMovieModal(props) {
|
||||
const {
|
||||
originalPath,
|
||||
destinationPath,
|
||||
destinationRootFolder,
|
||||
isOpen,
|
||||
onSavePress,
|
||||
onMoveSeriesPress
|
||||
onMoveMoviePress
|
||||
} = props;
|
||||
|
||||
if (
|
||||
@ -46,8 +46,8 @@ function MoveSeriesModal(props) {
|
||||
<ModalBody>
|
||||
{
|
||||
destinationRootFolder ?
|
||||
`Would you like to move the series folders to '${destinationRootFolder}'?` :
|
||||
`Would you like to move the series files from '${originalPath}' to '${destinationPath}'?`
|
||||
`Would you like to move the movie folders to '${destinationRootFolder}'?` :
|
||||
`Would you like to move the movie files from '${originalPath}' to '${destinationPath}'?`
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
@ -61,7 +61,7 @@ function MoveSeriesModal(props) {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={onMoveSeriesPress}
|
||||
onPress={onMoveMoviePress}
|
||||
>
|
||||
Yes, Move the Files
|
||||
</Button>
|
||||
@ -71,13 +71,13 @@ function MoveSeriesModal(props) {
|
||||
);
|
||||
}
|
||||
|
||||
MoveSeriesModal.propTypes = {
|
||||
MoveMovieModal.propTypes = {
|
||||
originalPath: PropTypes.string,
|
||||
destinationPath: PropTypes.string,
|
||||
destinationRootFolder: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onMoveSeriesPress: PropTypes.func.isRequired
|
||||
onMoveMoviePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MoveSeriesModal;
|
||||
export default MoveMovieModal;
|
@ -1,5 +1,5 @@
|
||||
.blankpad {
|
||||
padding-left:2em;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
@ -1,28 +1,28 @@
|
||||
.title {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
word-break: break-all;
|
||||
}
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.quality,
|
||||
.language {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
}
|
||||
.quality,
|
||||
.language {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
}
|
||||
|
||||
.language {
|
||||
width: 100px;
|
||||
}
|
||||
.language {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.rejected,
|
||||
.download {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
.rejected,
|
||||
.download {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 50px;
|
||||
}
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.age,
|
||||
.size {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
.age,
|
||||
.size {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
}
|
||||
|
||||
.blankpad {
|
||||
padding-left:2em;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.episodeFormat {
|
||||
.standardMovieFormat {
|
||||
margin-left: 5px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ class OrganizePreviewModalContent extends Component {
|
||||
error,
|
||||
items,
|
||||
renameEpisodes,
|
||||
episodeFormat,
|
||||
standardMovieFormat,
|
||||
path,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@ -129,8 +129,8 @@ class OrganizePreviewModalContent extends Component {
|
||||
|
||||
<div>
|
||||
Naming pattern:
|
||||
<span className={styles.episodeFormat}>
|
||||
{episodeFormat}
|
||||
<span className={styles.standardMovieFormat}>
|
||||
{standardMovieFormat}
|
||||
</span>
|
||||
</div>
|
||||
</Alert>
|
||||
@ -140,11 +140,11 @@ class OrganizePreviewModalContent extends Component {
|
||||
items.map((item) => {
|
||||
return (
|
||||
<OrganizePreviewRow
|
||||
key={item.episodeFileId}
|
||||
id={item.episodeFileId}
|
||||
key={item.movieFileId}
|
||||
id={item.movieFileId}
|
||||
existingPath={item.existingPath}
|
||||
newPath={item.newPath}
|
||||
isSelected={selectedState[item.episodeFileId]}
|
||||
isSelected={selectedState[item.movieFileId]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
@ -190,10 +190,9 @@ OrganizePreviewModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
seasonNumber: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
renameEpisodes: PropTypes.bool,
|
||||
episodeFormat: PropTypes.string,
|
||||
standardMovieFormat: PropTypes.string,
|
||||
onOrganizePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
@ -14,14 +14,14 @@ function createMapStateToProps() {
|
||||
(state) => state.organizePreview,
|
||||
(state) => state.settings.naming,
|
||||
createMovieSelector(),
|
||||
(organizePreview, naming, series) => {
|
||||
(organizePreview, naming, movie) => {
|
||||
const props = { ...organizePreview };
|
||||
props.isFetching = organizePreview.isFetching || naming.isFetching;
|
||||
props.isPopulated = organizePreview.isPopulated && naming.isPopulated;
|
||||
props.error = organizePreview.error || naming.error;
|
||||
props.renameEpisodes = naming.item.renameEpisodes;
|
||||
props.episodeFormat = naming.item.episodeFormat;
|
||||
props.path = series.path;
|
||||
props.standardMovieFormat = naming.item.standardMovieFormat;
|
||||
props.path = movie.path;
|
||||
|
||||
return props;
|
||||
}
|
||||
@ -41,13 +41,11 @@ class OrganizePreviewModalContentConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
seriesId,
|
||||
seasonNumber
|
||||
movieId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchOrganizePreview({
|
||||
seriesId,
|
||||
seasonNumber
|
||||
movieId
|
||||
});
|
||||
|
||||
this.props.fetchNamingSettings();
|
||||
@ -59,7 +57,7 @@ class OrganizePreviewModalContentConnector extends Component {
|
||||
onOrganizePress = (files) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.RENAME_FILES,
|
||||
seriesId: this.props.seriesId,
|
||||
movieId: this.props.movieId,
|
||||
files
|
||||
});
|
||||
|
||||
@ -80,8 +78,7 @@ class OrganizePreviewModalContentConnector extends Component {
|
||||
}
|
||||
|
||||
OrganizePreviewModalContentConnector.propTypes = {
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
seasonNumber: PropTypes.number,
|
||||
movieId: PropTypes.number.isRequired,
|
||||
fetchOrganizePreview: PropTypes.func.isRequired,
|
||||
fetchNamingSettings: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
|
@ -76,7 +76,7 @@ function EditRestrictionModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText="Restrictions will apply to series at least one matching tag. Leave blank to apply to all series"
|
||||
helpText="Restrictions will apply to movies at least one matching tag. Leave blank to apply to all movies"
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
@ -326,7 +326,7 @@ class MediaManagement extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="folderChmod"
|
||||
helpText="Octal, applied to series/season folders created by Radarr"
|
||||
helpText="Octal, applied to movie folders created by Radarr"
|
||||
values={fileDateOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.folderChmod}
|
||||
|
@ -98,7 +98,7 @@ function EditNotificationModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onGrab"
|
||||
helpText="Be notified when episodes are available for download and has been sent to a download client"
|
||||
helpText="Be notified when movies are available for download and has been sent to a download client"
|
||||
isDisabled={!supportsOnGrab.value}
|
||||
{...onGrab}
|
||||
onChange={onInputChange}
|
||||
@ -111,7 +111,7 @@ function EditNotificationModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onDownload"
|
||||
helpText="Be notified when episodes are successfully imported"
|
||||
helpText="Be notified when movies are successfully imported"
|
||||
isDisabled={!supportsOnDownload.value}
|
||||
{...onDownload}
|
||||
onChange={onInputChange}
|
||||
@ -126,7 +126,7 @@ function EditNotificationModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onUpgrade"
|
||||
helpText="Be notified when episodes are upgraded to a better quality"
|
||||
helpText="Be notified when movies are upgraded to a better quality"
|
||||
isDisabled={!supportsOnUpgrade.value}
|
||||
{...onUpgrade}
|
||||
onChange={onInputChange}
|
||||
@ -140,7 +140,7 @@ function EditNotificationModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onRename"
|
||||
helpText="Be notified when episodes are renamed"
|
||||
helpText="Be notified when movies are renamed"
|
||||
isDisabled={!supportsOnRename.value}
|
||||
{...onRename}
|
||||
onChange={onInputChange}
|
||||
@ -153,7 +153,7 @@ function EditNotificationModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText="Only send notifications for series with at least one matching tag"
|
||||
helpText="Only send notifications for movies with at least one matching tag"
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
@ -110,7 +110,7 @@ function EditDelayProfileModalContent(props) {
|
||||
{
|
||||
id === 1 ?
|
||||
<Alert>
|
||||
This is the default profile. It applies to all series that don't have an explicit profile.
|
||||
This is the default profile. It applies to all movies that don't have an explicit profile.
|
||||
</Alert> :
|
||||
|
||||
<FormGroup>
|
||||
@ -120,7 +120,7 @@ function EditDelayProfileModalContent(props) {
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
{...tags}
|
||||
helpText="Applies to series with at least one matching tag"
|
||||
helpText="Applies to movies with at least one matching tag"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -109,7 +109,7 @@ function Settings() {
|
||||
</Link>
|
||||
|
||||
<div className={styles.summary}>
|
||||
Create metadata files when episodes are imported or series are refreshed
|
||||
Create metadata files when movies are imported or refreshed
|
||||
</div>
|
||||
|
||||
<Link
|
||||
|
@ -190,8 +190,8 @@ export const actionHandlers = handleThunks({
|
||||
const item = _.find(items, { id });
|
||||
const selectedMovie = item.selectedMovie;
|
||||
|
||||
// Make sure we have a selected series and
|
||||
// the same series hasn't been added yet.
|
||||
// Make sure we have a selected movie and
|
||||
// the same movie hasn't been added yet.
|
||||
if (selectedMovie && !_.some(acc, { tmdbId: selectedMovie.tmdbId })) {
|
||||
const newMovie = getNewMovie(_.cloneDeep(selectedMovie), item);
|
||||
newMovie.path = item.path;
|
||||
|
@ -17,7 +17,6 @@ import * as queue from './queueActions';
|
||||
import * as releases from './releaseActions';
|
||||
import * as rootFolders from './rootFolderActions';
|
||||
import * as movies from './movieActions';
|
||||
import * as movieEditor from './movieEditorActions';
|
||||
import * as movieHistory from './movieHistoryActions';
|
||||
import * as movieIndex from './movieIndexActions';
|
||||
import * as settings from './settingsActions';
|
||||
@ -44,7 +43,6 @@ export default [
|
||||
releases,
|
||||
rootFolders,
|
||||
movies,
|
||||
movieEditor,
|
||||
movieHistory,
|
||||
movieIndex,
|
||||
settings,
|
||||
|
@ -1,179 +0,0 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { set, updateItem } from './baseActions';
|
||||
import { filters, filterPredicates, sortPredicates } from './movieActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'movieEditor';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
sortKey: 'sortTitle',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
secondarySortKey: 'sortTitle',
|
||||
secondarySortDirection: sortDirections.ASCENDING,
|
||||
selectedFilterKey: 'all',
|
||||
filters,
|
||||
filterPredicates,
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'monitored',
|
||||
label: 'Monitored',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.BOOL
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.SERIES_STATUS
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
label: 'Quality Profile',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.QUALITY_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
type: filterBuilderTypes.STRING
|
||||
},
|
||||
{
|
||||
name: 'rootFolderPath',
|
||||
label: 'Root Folder Path',
|
||||
type: filterBuilderTypes.EXACT
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
valueType: filterBuilderValueTypes.TAG
|
||||
}
|
||||
],
|
||||
|
||||
sortPredicates
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'movieEditor.sortKey',
|
||||
'movieEditor.sortDirection',
|
||||
'movieEditor.selectedFilterKey',
|
||||
'movieEditor.customFilters'
|
||||
];
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const SET_MOVIE_EDITOR_SORT = 'movieEditor/setMovieEditorSort';
|
||||
export const SET_MOVIE_EDITOR_FILTER = 'movieEditor/setMovieEditorFilter';
|
||||
export const SAVE_MOVIE_EDITOR = 'movieEditor/saveMovieEditor';
|
||||
export const BULK_DELETE_MOVIE = 'movieEditor/bulkDeleteMovie';
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const setMovieEditorSort = createAction(SET_MOVIE_EDITOR_SORT);
|
||||
export const setMovieEditorFilter = createAction(SET_MOVIE_EDITOR_FILTER);
|
||||
export const saveMovieEditor = createThunk(SAVE_MOVIE_EDITOR);
|
||||
export const bulkDeleteMovie = createThunk(BULK_DELETE_MOVIE);
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[SAVE_MOVIE_EDITOR]: function(getState, payload, dispatch) {
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/movie/editor',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify(payload),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
...data.map((movie) => {
|
||||
return updateItem({
|
||||
id: movie.id,
|
||||
section: 'movies',
|
||||
...movie
|
||||
});
|
||||
}),
|
||||
|
||||
set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[BULK_DELETE_MOVIE]: function(getState, payload, dispatch) {
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/movie/editor',
|
||||
method: 'DELETE',
|
||||
data: JSON.stringify(payload),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
// SignaR will take care of removing the series from the collection
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: false,
|
||||
deleteError: null
|
||||
}));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: false,
|
||||
deleteError: xhr
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_MOVIE_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||
[SET_MOVIE_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)
|
||||
|
||||
}, defaultState, section);
|
@ -1,10 +1,14 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { set, updateItem } from './baseActions';
|
||||
import { filters, filterPredicates, sortPredicates } from './movieActions';
|
||||
//
|
||||
// Variables
|
||||
@ -15,6 +19,10 @@ export const section = 'movieIndex';
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
sortKey: 'sortTitle',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
secondarySortKey: 'sortTitle',
|
||||
@ -47,6 +55,13 @@ export const defaultState = {
|
||||
},
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'select',
|
||||
columnLabel: 'select',
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
columnLabel: 'Status',
|
||||
@ -214,8 +229,8 @@ export const defaultState = {
|
||||
label: 'Genres',
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
optionsSelector: function(items) {
|
||||
const tagList = items.reduce((acc, series) => {
|
||||
series.genres.forEach((genre) => {
|
||||
const tagList = items.reduce((acc, movie) => {
|
||||
movie.genres.forEach((genre) => {
|
||||
acc.push({
|
||||
id: genre,
|
||||
name: genre
|
||||
@ -268,6 +283,8 @@ export const SET_MOVIE_VIEW = 'movieIndex/setMovieView';
|
||||
export const SET_MOVIE_TABLE_OPTION = 'movieIndex/setMovieTableOption';
|
||||
export const SET_MOVIE_POSTER_OPTION = 'movieIndex/setMoviePosterOption';
|
||||
export const SET_MOVIE_OVERVIEW_OPTION = 'movieIndex/setMovieOverviewOption';
|
||||
export const SAVE_MOVIE_EDITOR = 'movieIndex/saveMovieEditor';
|
||||
export const BULK_DELETE_MOVIE = 'movieIndex/bulkDeleteMovie';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
@ -278,6 +295,85 @@ export const setMovieView = createAction(SET_MOVIE_VIEW);
|
||||
export const setMovieTableOption = createAction(SET_MOVIE_TABLE_OPTION);
|
||||
export const setMoviePosterOption = createAction(SET_MOVIE_POSTER_OPTION);
|
||||
export const setMovieOverviewOption = createAction(SET_MOVIE_OVERVIEW_OPTION);
|
||||
export const saveMovieEditor = createThunk(SAVE_MOVIE_EDITOR);
|
||||
export const bulkDeleteMovie = createThunk(BULK_DELETE_MOVIE);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[SAVE_MOVIE_EDITOR]: function(getState, payload, dispatch) {
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/movie/editor',
|
||||
method: 'PUT',
|
||||
data: JSON.stringify(payload),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
...data.map((movie) => {
|
||||
return updateItem({
|
||||
id: movie.id,
|
||||
section: 'movies',
|
||||
...movie
|
||||
});
|
||||
}),
|
||||
|
||||
set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[BULK_DELETE_MOVIE]: function(getState, payload, dispatch) {
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: true
|
||||
}));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/movie/editor',
|
||||
method: 'DELETE',
|
||||
data: JSON.stringify(payload),
|
||||
dataType: 'json'
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
// SignaR will take care of removing the movie from the collection
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: false,
|
||||
deleteError: null
|
||||
}));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: false,
|
||||
deleteError: xhr
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
@ -8,10 +8,10 @@ function createImportMovieItemSelector() {
|
||||
(state) => state.addMovie,
|
||||
(state) => state.importMovie,
|
||||
createAllMoviesSelector(),
|
||||
(id, addMovie, importMovie, series) => {
|
||||
(id, addMovie, importMovie, movies) => {
|
||||
const item = _.find(importMovie.items, { id }) || {};
|
||||
const selectedMovie = item && item.selectedMovie;
|
||||
const isExistingMovie = !!selectedMovie && _.some(series, { tvdbId: selectedMovie.tvdbId });
|
||||
const isExistingMovie = !!selectedMovie && _.some(movies, { tmdbId: selectedMovie.tmdbId });
|
||||
|
||||
return {
|
||||
defaultMonitor: addMovie.defaults.monitor,
|
||||
|
@ -46,7 +46,7 @@ module.exports = {
|
||||
// Modal
|
||||
modalBodyPadding: '30px',
|
||||
|
||||
// Series
|
||||
// Movie
|
||||
movieIndexColumnPadding: '20px',
|
||||
movieIndexColumnPaddingSmallScreen: '10px',
|
||||
movieIndexOverviewInfoRowHeight: '21px'
|
||||
|
51
src/NzbDrone.Core/Movies/Commands/BulkMoveMovieCommand.cs
Normal file
51
src/NzbDrone.Core/Movies/Commands/BulkMoveMovieCommand.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Movies.Commands
|
||||
{
|
||||
public class BulkMoveMovieCommand : Command
|
||||
{
|
||||
public List<BulkMoveMovie> Movies { get; set; }
|
||||
public string DestinationRootFolder { get; set; }
|
||||
|
||||
public override bool SendUpdatesToClient => true;
|
||||
public override bool RequiresDiskAccess => true;
|
||||
}
|
||||
|
||||
public class BulkMoveMovie : IEquatable<BulkMoveMovie>
|
||||
{
|
||||
public int MovieId { get; set; }
|
||||
public string SourcePath { get; set; }
|
||||
|
||||
public bool Equals(BulkMoveMovie other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return MovieId.Equals(other.MovieId);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (obj.GetType() != GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return MovieId.Equals(((BulkMoveMovie)obj).MovieId);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return MovieId.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
@ -220,6 +220,7 @@
|
||||
<Compile Include="Movies\AlternativeTitles\AlternativeTitle.cs" />
|
||||
<Compile Include="Movies\AlternativeTitles\AlternativeTitleRepository.cs" />
|
||||
<Compile Include="Movies\AlternativeTitles\AlternativeTitleService.cs" />
|
||||
<Compile Include="Movies\Commands\BulkMoveMovieCommand.cs" />
|
||||
<Compile Include="Movies\Events\MoviesImportedEvent.cs" />
|
||||
<Compile Include="NetImport\NetImportListLevels.cs" />
|
||||
<Compile Include="NetImport\TMDb\TMDbLanguageCodes.cs" />
|
||||
|
@ -7,58 +7,98 @@
|
||||
using Radarr.Http.REST;
|
||||
using NzbDrone.Core.Movies;
|
||||
using Radarr.Http.Mapping;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Movies.Commands;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace Radarr.Api.V2.Movies
|
||||
{
|
||||
public class MovieEditorModule : RadarrV2Module
|
||||
{
|
||||
private readonly IMovieService _movieService;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
|
||||
public MovieEditorModule(IMovieService movieService)
|
||||
public MovieEditorModule(IMovieService movieService, IManageCommandQueue commandQueueManager)
|
||||
: base("/movie/editor")
|
||||
{
|
||||
_movieService = movieService;
|
||||
Put["/"] = Movie => SaveAll();
|
||||
Put["/delete"] = Movie => DeleteSelected();
|
||||
_commandQueueManager = commandQueueManager;
|
||||
Put["/"] = movie => SaveAll();
|
||||
Delete["/"] = movie => DeleteMovies();
|
||||
}
|
||||
|
||||
private Response SaveAll()
|
||||
{
|
||||
var resources = Request.Body.FromJson<List<MovieResource>>();
|
||||
var resource = Request.Body.FromJson<MovieEditorResource>();
|
||||
var moviesToUpdate = _movieService.GetMovies(resource.MovieIds);
|
||||
var moviesToMove = new List<BulkMoveMovie>();
|
||||
|
||||
var Movie = resources.Select(MovieResource => MovieResource.ToModel(_movieService.GetMovie(MovieResource.Id))).ToList();
|
||||
foreach (var movie in moviesToUpdate)
|
||||
{
|
||||
if (resource.Monitored.HasValue)
|
||||
{
|
||||
movie.Monitored = resource.Monitored.Value;
|
||||
}
|
||||
|
||||
return _movieService.UpdateMovie(Movie)
|
||||
if (resource.QualityProfileId.HasValue)
|
||||
{
|
||||
movie.ProfileId = resource.QualityProfileId.Value;
|
||||
}
|
||||
|
||||
if (resource.RootFolderPath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
movie.RootFolderPath = resource.RootFolderPath;
|
||||
moviesToMove.Add(new BulkMoveMovie
|
||||
{
|
||||
MovieId = movie.Id,
|
||||
SourcePath = movie.Path
|
||||
});
|
||||
}
|
||||
|
||||
if (resource.Tags != null)
|
||||
{
|
||||
var newTags = resource.Tags;
|
||||
var applyTags = resource.ApplyTags;
|
||||
|
||||
switch (applyTags)
|
||||
{
|
||||
case ApplyTags.Add:
|
||||
newTags.ForEach(t => movie.Tags.Add(t));
|
||||
break;
|
||||
case ApplyTags.Remove:
|
||||
newTags.ForEach(t => movie.Tags.Remove(t));
|
||||
break;
|
||||
case ApplyTags.Replace:
|
||||
movie.Tags = new HashSet<int>(newTags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resource.MoveFiles && moviesToMove.Any())
|
||||
{
|
||||
_commandQueueManager.Push(new BulkMoveMovieCommand
|
||||
{
|
||||
DestinationRootFolder = resource.RootFolderPath,
|
||||
Movies = moviesToMove
|
||||
});
|
||||
}
|
||||
|
||||
return _movieService.UpdateMovie(moviesToUpdate)
|
||||
.ToResource()
|
||||
.AsResponse(HttpStatusCode.Accepted);
|
||||
}
|
||||
|
||||
private Response DeleteSelected()
|
||||
private Response DeleteMovies()
|
||||
{
|
||||
var deleteFiles = false;
|
||||
var addExclusion = false;
|
||||
var deleteFilesQuery = Request.Query.deleteFiles;
|
||||
var addExclusionQuery = Request.Query.addExclusion;
|
||||
var resource = Request.Body.FromJson<MovieEditorResource>();
|
||||
|
||||
if (deleteFilesQuery.HasValue)
|
||||
foreach (var id in resource.MovieIds)
|
||||
{
|
||||
deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value);
|
||||
}
|
||||
if (addExclusionQuery.HasValue)
|
||||
{
|
||||
addExclusion = Convert.ToBoolean(addExclusionQuery.Value);
|
||||
}
|
||||
var ids = Request.Body.FromJson<List<int>>();
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
_movieService.DeleteMovie(id, deleteFiles, addExclusion);
|
||||
_movieService.DeleteMovie(id, false, false);
|
||||
}
|
||||
|
||||
return new Response
|
||||
{
|
||||
StatusCode = HttpStatusCode.Accepted
|
||||
};
|
||||
return new object().AsResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
src/Radarr.Api.V2/Movies/MovieEditorResource.cs
Normal file
24
src/Radarr.Api.V2/Movies/MovieEditorResource.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Movies;
|
||||
|
||||
namespace Radarr.Api.V2.Movies
|
||||
{
|
||||
class MovieEditorResource
|
||||
{
|
||||
public List<int> MovieIds { get; set; }
|
||||
public bool? Monitored { get; set; }
|
||||
public int? QualityProfileId { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public List<int> Tags { get; set; }
|
||||
public ApplyTags ApplyTags { get; set; }
|
||||
public bool MoveFiles { get; set; }
|
||||
}
|
||||
|
||||
public enum ApplyTags
|
||||
{
|
||||
Add,
|
||||
Remove,
|
||||
Replace
|
||||
}
|
||||
}
|
@ -136,6 +136,7 @@
|
||||
<Compile Include="Movies\AlternativeYearModule.cs" />
|
||||
<Compile Include="Movies\AlternativeYearResource.cs" />
|
||||
<Compile Include="Movies\FetchMovieListModule.cs" />
|
||||
<Compile Include="Movies\MovieEditorResource.cs" />
|
||||
<Compile Include="Movies\MovieImportModule.cs" />
|
||||
<Compile Include="Movies\MovieDiscoverModule.cs" />
|
||||
<Compile Include="Movies\MovieEditorModule.cs" />
|
||||
|
@ -42,7 +42,7 @@ public static JsonResponse<TModel> AsResponse<TModel>(this TModel model, HttpSta
|
||||
|
||||
public static IDictionary<string, string> DisableCache(this IDictionary<string, string> headers)
|
||||
{
|
||||
headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
|
||||
headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0";
|
||||
headers["Pragma"] = "no-cache";
|
||||
headers["Expires"] = "0";
|
||||
|
||||
|
@ -54,5 +54,17 @@ public static bool GetBooleanQueryParameter(this Request request, string paramet
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static int GetIntegerQueryParameter(this Request request, string parameter, int defaultValue = 0)
|
||||
{
|
||||
var parameterValue = request.Query[parameter];
|
||||
|
||||
if (parameterValue.HasValue)
|
||||
{
|
||||
return int.Parse(parameterValue.Value);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ public abstract class HtmlMapperBase : StaticResourceMapperBase
|
||||
{
|
||||
private readonly IDiskProvider _diskProvider;
|
||||
private readonly Func<ICacheBreakerProvider> _cacheBreakProviderFactory;
|
||||
private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src|json)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics|svg))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics|svg|json))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private string _generatedContent;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user