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
pull/3718/head
Qstick 5 years ago committed by GitHub
parent b8f7ca0749
commit a20222fbef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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-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 />
{
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}
/>
{
!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-left: 2em;
}

@ -2,27 +2,27 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
}
}
.quality,
.language {
.quality,
.language {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
}
.language {
.language {
width: 100px;
}
}
.rejected,
.download {
.rejected,
.download {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}
}
.age,
.size {
.age,
.size {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
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'

@ -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.Extensions;
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();
return _movieService.UpdateMovie(Movie)
.ToResource()
.AsResponse(HttpStatusCode.Accepted);
foreach (var movie in moviesToUpdate)
{
if (resource.Monitored.HasValue)
{
movie.Monitored = resource.Monitored.Value;
}
private Response DeleteSelected()
if (resource.QualityProfileId.HasValue)
{
var deleteFiles = false;
var addExclusion = false;
var deleteFilesQuery = Request.Query.deleteFiles;
var addExclusionQuery = Request.Query.addExclusion;
movie.ProfileId = resource.QualityProfileId.Value;
}
if (deleteFilesQuery.HasValue)
if (resource.RootFolderPath.IsNotNullOrWhiteSpace())
{
deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value);
movie.RootFolderPath = resource.RootFolderPath;
moviesToMove.Add(new BulkMoveMovie
{
MovieId = movie.Id,
SourcePath = movie.Path
});
}
if (addExclusionQuery.HasValue)
if (resource.Tags != null)
{
addExclusion = Convert.ToBoolean(addExclusionQuery.Value);
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;
}
}
}
var ids = Request.Body.FromJson<List<int>>();
foreach (var id in ids)
if (resource.MoveFiles && moviesToMove.Any())
{
_commandQueueManager.Push(new BulkMoveMovieCommand
{
_movieService.DeleteMovie(id, deleteFiles, addExclusion);
DestinationRootFolder = resource.RootFolderPath,
Movies = moviesToMove
});
}
return _movieService.UpdateMovie(moviesToUpdate)
.ToResource()
.AsResponse(HttpStatusCode.Accepted);
}
return new Response
private Response DeleteMovies()
{
var resource = Request.Body.FromJson<MovieEditorResource>();
foreach (var id in resource.MovieIds)
{
StatusCode = HttpStatusCode.Accepted
};
_movieService.DeleteMovie(id, false, false);
}
return new object().AsResponse();
}
}
}

@ -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 @@ namespace Radarr.Http.Extensions
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 @@ namespace Radarr.Http.Extensions
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 @@ namespace Radarr.Http.Frontend.Mappers
{
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…
Cancel
Save