1
0
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:
Qstick 2019-07-12 20:40:37 -04:00 committed by GitHub
parent b8f7ca0749
commit a20222fbef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 823 additions and 962 deletions

View File

@ -61,7 +61,7 @@ const mapDispatchToProps = {
dispatchImportMovie: importMovie, dispatchImportMovie: importMovie,
dispatchClearImportMovie: clearImportMovie, dispatchClearImportMovie: clearImportMovie,
dispatchFetchRootFolders: fetchRootFolders, dispatchFetchRootFolders: fetchRootFolders,
dispatchSetAddSeriesDefault: setAddMovieDefault dispatchSetAddMovieDefault: setAddMovieDefault
}; };
class ImportMovieConnector extends Component { class ImportMovieConnector extends Component {
@ -74,7 +74,7 @@ class ImportMovieConnector extends Component {
qualityProfiles, qualityProfiles,
defaultQualityProfileId, defaultQualityProfileId,
dispatchFetchRootFolders, dispatchFetchRootFolders,
dispatchSetAddSeriesDefault dispatchSetAddMovieDefault
} = this.props; } = this.props;
if (!this.props.rootFoldersPopulated) { if (!this.props.rootFoldersPopulated) {
@ -93,7 +93,7 @@ class ImportMovieConnector extends Component {
} }
if (setDefaults) { if (setDefaults) {
dispatchSetAddSeriesDefault(setDefaultPayload); dispatchSetAddMovieDefault(setDefaultPayload);
} }
} }
@ -105,7 +105,7 @@ class ImportMovieConnector extends Component {
// Listeners // Listeners
onInputChange = (ids, name, value) => { onInputChange = (ids, name, value) => {
this.props.dispatchSetAddSeriesDefault({ [name]: value }); this.props.dispatchSetAddMovieDefault({ [name]: value });
ids.forEach((id) => { ids.forEach((id) => {
this.props.dispatchSetImportMovieValue({ this.props.dispatchSetImportMovieValue({
@ -146,7 +146,7 @@ ImportMovieConnector.propTypes = {
dispatchImportMovie: PropTypes.func.isRequired, dispatchImportMovie: PropTypes.func.isRequired,
dispatchClearImportMovie: PropTypes.func.isRequired, dispatchClearImportMovie: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired, dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchSetAddSeriesDefault: PropTypes.func.isRequired dispatchSetAddMovieDefault: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieConnector); export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieConnector);

View File

@ -5,8 +5,8 @@ import { cancelLookupMovie } from 'Store/Actions/importMovieActions';
import ImportMovieFooter from './ImportMovieFooter'; import ImportMovieFooter from './ImportMovieFooter';
function isMixed(items, selectedIds, defaultValue, key) { function isMixed(items, selectedIds, defaultValue, key) {
return _.some(items, (series) => { return _.some(items, (movie) => {
return selectedIds.indexOf(series.id) > -1 && series[key] !== defaultValue; return selectedIds.indexOf(movie.id) > -1 && movie[key] !== defaultValue;
}); });
} }

View File

@ -23,7 +23,7 @@
min-width: 170px; min-width: 170px;
} }
.series { .movie {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css'; composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 400px; flex: 0 1 400px;

View File

@ -53,7 +53,7 @@ function ImportMovieRow(props) {
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
<VirtualTableRowCell className={styles.series}> <VirtualTableRowCell className={styles.movie}>
<ImportMovieSelectMovieConnector <ImportMovieSelectMovieConnector
id={id} id={id}
isExistingMovie={isExistingMovie} isExistingMovie={isExistingMovie}

View File

@ -66,23 +66,23 @@ class ImportMovieTable extends Component {
const isExistingMovie = !!selectedMovie && const isExistingMovie = !!selectedMovie &&
_.some(prevProps.allMovies, { tmdbId: selectedMovie.tmdbId }); _.some(prevProps.allMovies, { tmdbId: selectedMovie.tmdbId });
// Props doesn't have a selected series or // Props doesn't have a selected movie or
// the selected series is an existing series. // the selected movie is an existing movie.
if ((!selectedMovie && prevItem.selectedMovie) || (isExistingMovie && !prevItem.selectedMovie)) { if ((!selectedMovie && prevItem.selectedMovie) || (isExistingMovie && !prevItem.selectedMovie)) {
onSelectedChange({ id, value: false }); onSelectedChange({ id, value: false });
return; return;
} }
// State is selected, but a series isn't selected or // State is selected, but a movie isn't selected or
// the selected series is an existing series. // the selected movie is an existing movie.
if (isSelected && (!selectedMovie || isExistingMovie)) { if (isSelected && (!selectedMovie || isExistingMovie)) {
onSelectedChange({ id, value: false }); onSelectedChange({ id, value: false });
return; 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) { if (selectedMovie && selectedMovie !== prevItem.selectedMovie) {
onSelectedChange({ id, value: true }); onSelectedChange({ id, value: true });

View File

@ -1,4 +1,4 @@
.series { .movie {
padding: 10px 20px; padding: 10px 20px;
width: 100%; width: 100%;

View File

@ -26,7 +26,7 @@ class ImportMovieSearchResult extends Component {
return ( return (
<Link <Link
className={styles.series} className={styles.movie}
onPress={this.onPress} onPress={this.onPress}
> >
<ImportMovieTitle <ImportMovieTitle

View File

@ -259,7 +259,7 @@ class ImportMovieSelectMovie extends Component {
title={item.title} title={item.title}
year={item.year} year={item.year}
studio={item.studio} studio={item.studio}
onPress={this.onSeriesSelect} onPress={this.onMovieSelect}
/> />
); );
}) })

View File

@ -7,7 +7,6 @@ import Switch from 'Components/Router/Switch';
import MovieIndexConnector from 'Movie/Index/MovieIndexConnector'; import MovieIndexConnector from 'Movie/Index/MovieIndexConnector';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector'; import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies'; import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import SeriesEditorConnector from 'Movie/Editor/SeriesEditorConnector';
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector'; import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
import CalendarPageConnector from 'Calendar/CalendarPageConnector'; import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import HistoryConnector from 'Activity/History/HistoryConnector'; import HistoryConnector from 'Activity/History/HistoryConnector';
@ -77,11 +76,6 @@ function AppRoutes(props) {
component={ImportMovies} component={ImportMovies}
/> />
<Route
path="/serieseditor"
component={SeriesEditorConnector}
/>
<Route <Route
path="/movie/:titleSlug" path="/movie/:titleSlug"
component={MovieDetailsPageConnector} component={MovieDetailsPageConnector}

View File

@ -163,7 +163,7 @@ class CalendarLinkModalContent extends Component {
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
value={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} onChange={this.onInputChange}
/> />
</FormGroup> </FormGroup>

View File

@ -12,7 +12,7 @@ export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch';
export const MOVE_MOVIE = 'MoveMovie'; export const MOVE_MOVIE = 'MoveMovie';
export const REFRESH_MOVIE = 'RefreshMovie'; export const REFRESH_MOVIE = 'RefreshMovie';
export const RENAME_FILES = 'RenameFiles'; export const RENAME_FILES = 'RenameFiles';
export const RENAME_SERIES = 'RenameSeries'; export const RENAME_MOVIE = 'RenameMovie';
export const RESET_API_KEY = 'ResetApiKey'; export const RESET_API_KEY = 'ResetApiKey';
export const RSS_SYNC = 'RssSync'; export const RSS_SYNC = 'RssSync';
export const MOVIE_SEARCH = 'MoviesSearch'; export const MOVIE_SEARCH = 'MoviesSearch';

View File

@ -7,7 +7,7 @@ import styles from './MonitorToggleButton.css';
function getTooltip(monitored, isDisabled) { function getTooltip(monitored, isDisabled) {
if (isDisabled) { if (isDisabled) {
return 'Cannot toogle monitored state when series is unmonitored'; return 'Cannot toogle monitored state when movie is unmonitored';
} }
if (monitored) { if (monitored) {

View File

@ -136,8 +136,8 @@ class MovieSearchInput extends Component {
return; return;
} }
// If an suggestion is not selected go to the first series, // If an suggestion is not selected go to the first movie,
// otherwise go to the selected series. // otherwise go to the selected movie.
if (highlightedSuggestionIndex == null) { if (highlightedSuggestionIndex == null) {
this.goToMovie(suggestions[0]); this.goToMovie(suggestions[0]);

View File

@ -19,7 +19,7 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [ const links = [
{ {
iconName: icons.SERIES_CONTINUING, iconName: icons.MOVIE_CONTINUING,
title: 'Movies', title: 'Movies',
to: '/', to: '/',
alias: '/movies', alias: '/movies',

View File

@ -34,6 +34,7 @@ import {
faCalendarAlt as fasCalendarAlt, faCalendarAlt as fasCalendarAlt,
faCaretDown as fasCaretDown, faCaretDown as fasCaretDown,
faCheck as fasCheck, faCheck as fasCheck,
faCheckSquare as fasCheckSquare,
faChevronCircleDown as fasChevronCircleDown, faChevronCircleDown as fasChevronCircleDown,
faChevronCircleRight as fasChevronCircleRight, faChevronCircleRight as fasChevronCircleRight,
faChevronCircleUp as fasChevronCircleUp, faChevronCircleUp as fasChevronCircleUp,
@ -117,6 +118,7 @@ export const CARET_DOWN = fasCaretDown;
export const CHECK = fasCheck; export const CHECK = fasCheck;
export const CHECK_INDETERMINATE = fasMinus; export const CHECK_INDETERMINATE = fasMinus;
export const CHECK_CIRCLE = fasCheckCircle; export const CHECK_CIRCLE = fasCheckCircle;
export const CHECK_SQUARE = fasCheckSquare;
export const CIRCLE = fasCircle; export const CIRCLE = fasCircle;
export const CIRCLE_OUTLINE = farCircle; export const CIRCLE_OUTLINE = farCircle;
export const CLEAR = fasTrashAlt; export const CLEAR = fasTrashAlt;
@ -180,7 +182,7 @@ export const SAVE = fasSave;
export const SCHEDULED = farClock; export const SCHEDULED = farClock;
export const SCORE = fasUserPlus; export const SCORE = fasUserPlus;
export const SEARCH = fasSearch; export const SEARCH = fasSearch;
export const SERIES_CONTINUING = fasPlay; export const MOVIE_CONTINUING = fasPlay;
export const SERIES_ENDED = fasStop; export const SERIES_ENDED = fasStop;
export const SETTINGS = fasCogs; export const SETTINGS = fasCogs;
export const SHUTDOWN = fasPowerOff; export const SHUTDOWN = fasPowerOff;

View File

@ -527,7 +527,7 @@ class MovieDetails extends Component {
<InteractiveImportModal <InteractiveImportModal
isOpen={isInteractiveImportModalOpen} isOpen={isInteractiveImportModalOpen}
folder={path} folder={path}
allowSeriesChange={false} allowMovieChange={false}
showFilterExistingFiles={true} showFilterExistingFiles={true}
showImportMode={false} showImportMode={false}
onModalClose={this.onInteractiveImportModalClose} onModalClose={this.onInteractiveImportModalClose}

View File

@ -69,7 +69,7 @@ function createMapStateToProps() {
const isRefreshing = isMovieRefreshing || allMoviesRefreshing; const isRefreshing = isMovieRefreshing || allMoviesRefreshing;
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.MOVIE_SEARCH, movieIds: [movie.id] })); 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 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 = ( const isRenamingMovie = (
isCommandExecuting(isRenamingMovieCommand) && isCommandExecuting(isRenamingMovieCommand) &&
isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1 isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1

View File

@ -54,7 +54,7 @@ class MovieDetailsPageConnector extends Component {
if (!titleSlug) { if (!titleSlug) {
return ( return (
<NotFound <NotFound
message="Sorry, that series cannot be found." message="Sorry, that movie cannot be found."
/> />
); );
} }

View File

@ -11,7 +11,7 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import MoveMovieModal from 'Movie/MoveSeries/MoveSeriesModal'; import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import styles from './EditMovieModalContent.css'; import styles from './EditMovieModalContent.css';
class EditMovieModalContent extends Component { class EditMovieModalContent extends Component {
@ -45,7 +45,7 @@ class EditMovieModalContent extends Component {
} }
} }
onMoveSeriesPress = () => { onMoveMoviePress = () => {
this.setState({ isConfirmMoveModalOpen: false }); this.setState({ isConfirmMoveModalOpen: false });
this.props.onSavePress(true); this.props.onSavePress(true);
@ -159,7 +159,7 @@ class EditMovieModalContent extends Component {
destinationPath={path.value} destinationPath={path.value}
isOpen={this.state.isConfirmMoveModalOpen} isOpen={this.state.isConfirmMoveModalOpen}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onMoveSeriesPress={this.onMoveSeriesPress} onMoveMoviePress={this.onMoveMoviePress}
/> />
</ModalContent> </ModalContent>
); );

View File

@ -29,21 +29,21 @@ function createMapStateToProps() {
(state) => state.movies, (state) => state.movies,
createMovieSelector(), createMovieSelector(),
createIsPathChangingSelector(), createIsPathChangingSelector(),
(seriesState, movie, isPathChanging) => { (moviesState, movie, isPathChanging) => {
const { const {
isSaving, isSaving,
saveError, saveError,
pendingChanges pendingChanges
} = seriesState; } = moviesState;
const seriesSettings = _.pick(movie, [ const movieSettings = _.pick(movie, [
'monitored', 'monitored',
'qualityProfileId', 'qualityProfileId',
'path', 'path',
'tags' 'tags'
]); ]);
const settings = selectSettings(seriesSettings, pendingChanges, saveError); const settings = selectSettings(movieSettings, pendingChanges, saveError);
return { return {
title: movie.title, title: movie.title,
@ -97,7 +97,7 @@ class EditMovieModalContentConnector extends Component {
{...this.props} {...this.props}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onMoveSeriesPress={this.onMoveSeriesPress} onMoveMoviePress={this.onMoveMoviePress}
/> />
); );
} }

View File

@ -43,7 +43,7 @@ class DeleteMovieModalContent extends Component {
render() { render() {
const { const {
series, movies,
onModalClose onModalClose
} = this.props; } = this.props;
const deleteFiles = this.state.deleteFiles; const deleteFiles = this.state.deleteFiles;
@ -51,19 +51,19 @@ class DeleteMovieModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Delete Selected Series Delete Selected Movie(s)
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
<FormGroup> <FormGroup>
<FormLabel>{`Delete Series Folder${series.length > 1 ? 's' : ''}`}</FormLabel> <FormLabel>{`Delete Movie Folder${movies.length > 1 ? 's' : ''}`}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="deleteFiles" name="deleteFiles"
value={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} kind={kinds.DANGER}
onChange={this.onDeleteFilesChange} onChange={this.onDeleteFilesChange}
/> />
@ -71,12 +71,12 @@ class DeleteMovieModalContent extends Component {
</div> </div>
<div className={styles.message}> <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> </div>
<ul> <ul>
{ {
series.map((s) => { movies.map((s) => {
return ( return (
<li key={s.title}> <li key={s.title}>
<span>{s.title}</span> <span>{s.title}</span>
@ -115,7 +115,7 @@ class DeleteMovieModalContent extends Component {
} }
DeleteMovieModalContent.propTypes = { DeleteMovieModalContent.propTypes = {
series: PropTypes.arrayOf(PropTypes.object).isRequired, movies: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onDeleteSelectedPress: PropTypes.func.isRequired onDeleteSelectedPress: PropTypes.func.isRequired
}; };

View File

@ -2,20 +2,20 @@ import _ from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import { bulkDeleteMovie } from 'Store/Actions/movieEditorActions'; import { bulkDeleteMovie } from 'Store/Actions/movieIndexActions';
import DeleteMovieModalContent from './DeleteMovieModalContent'; import DeleteMovieModalContent from './DeleteMovieModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { seriesIds }) => seriesIds, (state, { movieIds }) => movieIds,
createAllMoviesSelector(), createAllMoviesSelector(),
(seriesIds, allMovies) => { (movieIds, allMovies) => {
const selectedMovie = _.intersectionWith(allMovies, seriesIds, (s, id) => { const selectedMovie = _.intersectionWith(allMovies, movieIds, (s, id) => {
return s.id === id; return s.id === id;
}); });
const sortedSeries = _.orderBy(selectedMovie, 'sortTitle'); const sortedMovies = _.orderBy(selectedMovie, 'sortTitle');
const series = _.map(sortedSeries, (s) => { const movies = _.map(sortedMovies, (s) => {
return { return {
title: s.title, title: s.title,
path: s.path path: s.path
@ -23,7 +23,7 @@ function createMapStateToProps() {
}); });
return { return {
series movies
}; };
} }
); );
@ -33,7 +33,7 @@ function createMapDispatchToProps(dispatch, props) {
return { return {
onDeleteSelectedPress(deleteFiles) { onDeleteSelectedPress(deleteFiles) {
dispatch(bulkDeleteMovie({ dispatch(bulkDeleteMovie({
seriesIds: props.seriesIds, movieIds: props.movieIds,
deleteFiles deleteFiles
})); }));

View File

@ -6,15 +6,15 @@ import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSe
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector'; import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import MoveSeriesModal from 'Movie/MoveSeries/MoveSeriesModal'; import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import TagsModal from './Tags/TagsModal'; import TagsModal from './Tags/TagsModal';
import DeleteMovieModal from './Delete/DeleteMovieModal'; import DeleteMovieModal from './Delete/DeleteMovieModal';
import SeriesEditorFooterLabel from './SeriesEditorFooterLabel'; import MovieEditorFooterLabel from './MovieEditorFooterLabel';
import styles from './SeriesEditorFooter.css'; import styles from './MovieEditorFooter.css';
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
class SeriesEditorFooter extends Component { class MovieEditorFooter extends Component {
// //
// Lifecycle // Lifecycle
@ -112,7 +112,7 @@ class SeriesEditorFooter extends Component {
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder }); this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
} }
onMoveSeriesPress = () => { onMoveMoviePress = () => {
this.setState({ this.setState({
isConfirmMoveModalOpen: false, isConfirmMoveModalOpen: false,
destinationRootFolder: null destinationRootFolder: null
@ -129,12 +129,12 @@ class SeriesEditorFooter extends Component {
render() { render() {
const { const {
seriesIds, movieIds,
selectedCount, selectedCount,
isSaving, isSaving,
isDeleting, isDeleting,
isOrganizingSeries, isOrganizingMovie,
onOrganizeSeriesPress onOrganizeMoviePress
} = this.props; } = this.props;
const { const {
@ -157,8 +157,8 @@ class SeriesEditorFooter extends Component {
return ( return (
<PageContentFooter> <PageContentFooter>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<SeriesEditorFooterLabel <MovieEditorFooterLabel
label="Monitor Series" label="Monitor Movie"
isSaving={isSaving && monitored !== NO_CHANGE} isSaving={isSaving && monitored !== NO_CHANGE}
/> />
@ -172,7 +172,7 @@ class SeriesEditorFooter extends Component {
</div> </div>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<SeriesEditorFooterLabel <MovieEditorFooterLabel
label="Quality Profile" label="Quality Profile"
isSaving={isSaving && qualityProfileId !== NO_CHANGE} isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/> />
@ -187,7 +187,7 @@ class SeriesEditorFooter extends Component {
</div> </div>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<SeriesEditorFooterLabel <MovieEditorFooterLabel
label="Root Folder" label="Root Folder"
isSaving={isSaving && rootFolderPath !== NO_CHANGE} isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/> />
@ -204,8 +204,8 @@ class SeriesEditorFooter extends Component {
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}> <div className={styles.buttonContainerContent}>
<SeriesEditorFooterLabel <MovieEditorFooterLabel
label={`${selectedCount} Series Selected`} label={`${selectedCount} Movie(s) Selected`}
isSaving={false} isSaving={false}
/> />
@ -214,9 +214,9 @@ class SeriesEditorFooter extends Component {
<SpinnerButton <SpinnerButton
className={styles.organizeSelectedButton} className={styles.organizeSelectedButton}
kind={kinds.WARNING} kind={kinds.WARNING}
isSpinning={isOrganizingSeries} isSpinning={isOrganizingMovie}
isDisabled={!selectedCount || isOrganizingSeries} isDisabled={!selectedCount || isOrganizingMovie}
onPress={onOrganizeSeriesPress} onPress={onOrganizeMoviePress}
> >
Rename Files Rename Files
</SpinnerButton> </SpinnerButton>
@ -224,7 +224,7 @@ class SeriesEditorFooter extends Component {
<SpinnerButton <SpinnerButton
className={styles.tagsButton} className={styles.tagsButton}
isSpinning={isSaving && savingTags} isSpinning={isSaving && savingTags}
isDisabled={!selectedCount || isOrganizingSeries} isDisabled={!selectedCount || isOrganizingMovie}
onPress={this.onTagsPress} onPress={this.onTagsPress}
> >
Set Tags Set Tags
@ -246,38 +246,38 @@ class SeriesEditorFooter extends Component {
<TagsModal <TagsModal
isOpen={isTagsModalOpen} isOpen={isTagsModalOpen}
seriesIds={seriesIds} movieIds={movieIds}
onApplyTagsPress={this.onApplyTagsPress} onApplyTagsPress={this.onApplyTagsPress}
onModalClose={this.onTagsModalClose} onModalClose={this.onTagsModalClose}
/> />
<DeleteMovieModal <DeleteMovieModal
isOpen={isDeleteMovieModalOpen} isOpen={isDeleteMovieModalOpen}
seriesIds={seriesIds} movieIds={movieIds}
onModalClose={this.onDeleteMovieModalClose} onModalClose={this.onDeleteMovieModalClose}
/> />
<MoveSeriesModal <MoveMovieModal
destinationRootFolder={destinationRootFolder} destinationRootFolder={destinationRootFolder}
isOpen={isConfirmMoveModalOpen} isOpen={isConfirmMoveModalOpen}
onSavePress={this.onSaveRootFolderPress} onSavePress={this.onSaveRootFolderPress}
onMoveSeriesPress={this.onMoveSeriesPress} onMoveMoviePress={this.onMoveMoviePress}
/> />
</PageContentFooter> </PageContentFooter>
); );
} }
} }
SeriesEditorFooter.propTypes = { MovieEditorFooter.propTypes = {
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired, movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired, selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired, isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object, deleteError: PropTypes.object,
isOrganizingSeries: PropTypes.bool.isRequired, isOrganizingMovie: PropTypes.bool.isRequired,
onSaveSelected: PropTypes.func.isRequired, onSaveSelected: PropTypes.func.isRequired,
onOrganizeSeriesPress: PropTypes.func.isRequired onOrganizeMoviePress: PropTypes.func.isRequired
}; };
export default SeriesEditorFooter; export default MovieEditorFooter;

View File

@ -2,9 +2,9 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import SpinnerIcon from 'Components/SpinnerIcon'; import SpinnerIcon from 'Components/SpinnerIcon';
import styles from './SeriesEditorFooterLabel.css'; import styles from './MovieEditorFooterLabel.css';
function SeriesEditorFooterLabel(props) { function MovieEditorFooterLabel(props) {
const { const {
className, className,
label, label,
@ -27,14 +27,14 @@ function SeriesEditorFooterLabel(props) {
); );
} }
SeriesEditorFooterLabel.propTypes = { MovieEditorFooterLabel.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired isSaving: PropTypes.bool.isRequired
}; };
SeriesEditorFooterLabel.defaultProps = { MovieEditorFooterLabel.defaultProps = {
className: styles.label className: styles.label
}; };
export default SeriesEditorFooterLabel; export default MovieEditorFooterLabel;

View File

@ -1,9 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import OrganizeSeriesModalContentConnector from './OrganizeSeriesModalContentConnector'; import OrganizeMovieModalContentConnector from './OrganizeMovieModalContentConnector';
function OrganizeSeriesModal(props) { function OrganizeMovieModal(props) {
const { const {
isOpen, isOpen,
onModalClose, onModalClose,
@ -15,7 +15,7 @@ function OrganizeSeriesModal(props) {
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
<OrganizeSeriesModalContentConnector <OrganizeMovieModalContentConnector
{...otherProps} {...otherProps}
onModalClose={onModalClose} onModalClose={onModalClose}
/> />
@ -23,9 +23,9 @@ function OrganizeSeriesModal(props) {
); );
} }
OrganizeSeriesModal.propTypes = { OrganizeMovieModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default OrganizeSeriesModal; export default OrganizeMovieModal;

View File

@ -8,24 +8,24 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import styles from './OrganizeSeriesModalContent.css'; import styles from './OrganizeMovieModalContent.css';
function OrganizeSeriesModalContent(props) { function OrganizeMovieModalContent(props) {
const { const {
seriesTitles, movieTitles,
onModalClose, onModalClose,
onOrganizeSeriesPress onOrganizeMoviePress
} = props; } = props;
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Organize Selected Series Organize Selected Movies
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<Alert> <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 <Icon
className={styles.renameIcon} className={styles.renameIcon}
name={icons.ORGANIZE} name={icons.ORGANIZE}
@ -33,12 +33,12 @@ function OrganizeSeriesModalContent(props) {
</Alert> </Alert>
<div className={styles.message}> <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> </div>
<ul> <ul>
{ {
seriesTitles.map((title) => { movieTitles.map((title) => {
return ( return (
<li key={title}> <li key={title}>
{title} {title}
@ -56,7 +56,7 @@ function OrganizeSeriesModalContent(props) {
<Button <Button
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onOrganizeSeriesPress} onPress={onOrganizeMoviePress}
> >
Organize Organize
</Button> </Button>
@ -65,10 +65,10 @@ function OrganizeSeriesModalContent(props) {
); );
} }
OrganizeSeriesModalContent.propTypes = { OrganizeMovieModalContent.propTypes = {
seriesTitles: PropTypes.arrayOf(PropTypes.string).isRequired, movieTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onOrganizeSeriesPress: PropTypes.func.isRequired onOrganizeMoviePress: PropTypes.func.isRequired
}; };
export default OrganizeSeriesModalContent; export default OrganizeMovieModalContent;

View File

@ -6,22 +6,22 @@ import { createSelector } from 'reselect';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import OrganizeSeriesModalContent from './OrganizeSeriesModalContent'; import OrganizeMovieModalContent from './OrganizeMovieModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { seriesIds }) => seriesIds, (state, { movieIds }) => movieIds,
createAllMoviesSelector(), createAllMoviesSelector(),
(seriesIds, allMovies) => { (movieIds, allMovies) => {
const series = _.intersectionWith(allMovies, seriesIds, (s, id) => { const movies = _.intersectionWith(allMovies, movieIds, (s, id) => {
return s.id === id; return s.id === id;
}); });
const sortedSeries = _.orderBy(series, 'sortTitle'); const sortedMovies = _.orderBy(movies, 'sortTitle');
const seriesTitles = _.map(sortedSeries, 'title'); const movieTitles = _.map(sortedMovies, 'title');
return { return {
seriesTitles movieTitles
}; };
} }
); );
@ -31,15 +31,15 @@ const mapDispatchToProps = {
executeCommand executeCommand
}; };
class OrganizeSeriesModalContentConnector extends Component { class OrganizeMovieModalContentConnector extends Component {
// //
// Listeners // Listeners
onOrganizeSeriesPress = () => { onOrganizeMoviePress = () => {
this.props.executeCommand({ this.props.executeCommand({
name: commandNames.RENAME_SERIES, name: commandNames.RENAME_MOVIE,
seriesIds: this.props.seriesIds movieIds: this.props.movieIds
}); });
this.props.onModalClose(true); this.props.onModalClose(true);
@ -50,18 +50,18 @@ class OrganizeSeriesModalContentConnector extends Component {
render(props) { render(props) {
return ( return (
<OrganizeSeriesModalContent <OrganizeMovieModalContent
{...this.props} {...this.props}
onOrganizeSeriesPress={this.onOrganizeSeriesPress} onOrganizeMoviePress={this.onOrganizeMoviePress}
/> />
); );
} }
} }
OrganizeSeriesModalContentConnector.propTypes = { OrganizeMovieModalContentConnector.propTypes = {
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired, movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired executeCommand: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeSeriesModalContentConnector); export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeMovieModalContentConnector);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -49,7 +49,7 @@ class TagsModalContent extends Component {
render() { render() {
const { const {
seriesTags, movieTags,
tagList, tagList,
onModalClose onModalClose
} = this.props; } = this.props;
@ -93,7 +93,7 @@ class TagsModalContent extends Component {
value={applyTags} value={applyTags}
values={applyTagsOptions} values={applyTagsOptions}
helpTexts={[ 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', 'Add: Add the tags the existing list of tags',
'Remove: Remove the entered tags', 'Remove: Remove the entered tags',
'Replace: Replace the tags with the entered tags (enter no tags to clear all 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}> <div className={styles.result}>
{ {
seriesTags.map((t) => { movieTags.map((t) => {
const tag = _.find(tagList, { id: t }); const tag = _.find(tagList, { id: t });
if (!tag) { if (!tag) {
@ -139,7 +139,7 @@ class TagsModalContent extends Component {
return null; return null;
} }
if (seriesTags.indexOf(t) > -1) { if (movieTags.indexOf(t) > -1) {
return null; return null;
} }
@ -178,7 +178,7 @@ class TagsModalContent extends Component {
} }
TagsModalContent.propTypes = { TagsModalContent.propTypes = {
seriesTags: PropTypes.arrayOf(PropTypes.number).isRequired, movieTags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired, tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onApplyTagsPress: PropTypes.func.isRequired onApplyTagsPress: PropTypes.func.isRequired

View File

@ -7,18 +7,18 @@ import TagsModalContent from './TagsModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { seriesIds }) => seriesIds, (state, { movieIds }) => movieIds,
createAllMoviesSelector(), createAllMoviesSelector(),
createTagsSelector(), createTagsSelector(),
(seriesIds, allMovies, tagList) => { (movieIds, allMovies, tagList) => {
const series = _.intersectionWith(allMovies, seriesIds, (s, id) => { const movies = _.intersectionWith(allMovies, movieIds, (s, id) => {
return s.id === id; return s.id === id;
}); });
const seriesTags = _.uniq(_.concat(..._.map(series, 'tags'))); const movieTags = _.uniq(_.concat(..._.map(movies, 'tags')));
return { return {
seriesTags, movieTags,
tagList tagList
}; };
} }

View File

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

View File

@ -2,6 +2,9 @@ import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; 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 { align, icons, sortDirections } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
@ -23,7 +26,9 @@ import MovieIndexFilterMenu from './Menus/MovieIndexFilterMenu';
import MovieIndexSortMenu from './Menus/MovieIndexSortMenu'; import MovieIndexSortMenu from './Menus/MovieIndexSortMenu';
import MovieIndexViewMenu from './Menus/MovieIndexViewMenu'; import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
import MovieIndexFooterConnector from './MovieIndexFooterConnector'; import MovieIndexFooterConnector from './MovieIndexFooterConnector';
import MovieEditorFooter from 'Movie/Editor/MovieEditorFooter.js';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import OrganizeMovieModal from 'Movie/Editor/Organize/OrganizeMovieModal';
import styles from './MovieIndex.css'; import styles from './MovieIndex.css';
function getViewComponent(view) { function getViewComponent(view) {
@ -53,12 +58,19 @@ class MovieIndex extends Component {
isPosterOptionsModalOpen: false, isPosterOptionsModalOpen: false,
isOverviewOptionsModalOpen: false, isOverviewOptionsModalOpen: false,
isInteractiveImportModalOpen: false, isInteractiveImportModalOpen: false,
isMovieEditorActive: false,
isOrganizingMovieModalOpen: false,
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isRendered: false isRendered: false
}; };
} }
componentDidMount() { componentDidMount() {
this.setJumpBarItems(); this.setJumpBarItems();
this.setSelectedState();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@ -66,7 +78,9 @@ class MovieIndex extends Component {
items, items,
sortKey, sortKey,
sortDirection, sortDirection,
scrollTop scrollTop,
isDeleting,
deleteError
} = this.props; } = this.props;
if ( if (
@ -75,11 +89,20 @@ class MovieIndex extends Component {
sortDirection !== prevProps.sortDirection sortDirection !== prevProps.sortDirection
) { ) {
this.setJumpBarItems(); this.setJumpBarItems();
this.setSelectedState();
} }
if (this.state.jumpToCharacter != null && scrollTop !== prevProps.scrollTop) { if (this.state.jumpToCharacter != null && scrollTop !== prevProps.scrollTop) {
this.setState({ jumpToCharacter: null }); 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 }); 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() { setJumpBarItems() {
const { const {
items, items,
@ -149,10 +211,51 @@ class MovieIndex extends Component {
this.setState({ isInteractiveImportModalOpen: false }); this.setState({ isInteractiveImportModalOpen: false });
} }
onMovieEditorTogglePress = () => {
if (this.state.isMovieEditorActive) {
this.setState({ isMovieEditorActive: false });
} else {
this.setState({ isMovieEditorActive: true });
}
}
onJumpBarItemPress = (jumpToCharacter) => { onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ 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 = () => { onRender = () => {
this.setState({ isRendered: true }, () => { this.setState({ isRendered: true }, () => {
const { const {
@ -193,6 +296,11 @@ class MovieIndex extends Component {
view, view,
isRefreshingMovie, isRefreshingMovie,
isRssSyncExecuting, isRssSyncExecuting,
isOrganizingMovie,
isSaving,
saveError,
isDeleting,
deleteError,
scrollTop, scrollTop,
onSortSelect, onSortSelect,
onFilterSelect, onFilterSelect,
@ -209,9 +317,15 @@ class MovieIndex extends Component {
isPosterOptionsModalOpen, isPosterOptionsModalOpen,
isOverviewOptionsModalOpen, isOverviewOptionsModalOpen,
isInteractiveImportModalOpen, isInteractiveImportModalOpen,
isRendered isMovieEditorActive,
isRendered,
selectedState,
allSelected,
allUnselected
} = this.state; } = this.state;
const selectedMovieIds = this.getSelectedIds();
const ViewComponent = getViewComponent(view); const ViewComponent = getViewComponent(view);
const isLoaded = !!(!error && isPopulated && items.length && contentBody); const isLoaded = !!(!error && isPopulated && items.length && contentBody);
const hasNoMovie = !totalItems; const hasNoMovie = !totalItems;
@ -248,16 +362,38 @@ class MovieIndex extends Component {
<PageToolbarButton <PageToolbarButton
label="Manual Import" label="Manual Import"
iconName={icons.INTERACTIVE} iconName={icons.INTERACTIVE}
isDisabled={hasNoMovie}
onPress={this.onInteractiveImportPress} onPress={this.onInteractiveImportPress}
/> />
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton {
label="Movie Editor" isMovieEditorActive ?
iconName={icons.EDIT} <PageToolbarButton
isDisabled={hasNoMovie} 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> </PageToolbarSection>
@ -360,10 +496,19 @@ class MovieIndex extends Component {
scrollTop={scrollTop} scrollTop={scrollTop}
jumpToCharacter={jumpToCharacter} jumpToCharacter={jumpToCharacter}
onRender={this.onRender} onRender={this.onRender}
isMovieEditorActive={isMovieEditorActive}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps} {...otherProps}
/> />
<MovieIndexFooterConnector /> {
!isMovieEditorActive &&
<MovieIndexFooterConnector />
}
</div> </div>
} }
@ -382,6 +527,21 @@ class MovieIndex extends Component {
} }
</div> </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 <MovieIndexPosterOptionsModal
isOpen={isPosterOptionsModalOpen} isOpen={isPosterOptionsModalOpen}
onModalClose={this.onPosterOptionsModalClose} onModalClose={this.onPosterOptionsModalClose}
@ -396,6 +556,12 @@ class MovieIndex extends Component {
isOpen={isInteractiveImportModalOpen} isOpen={isInteractiveImportModalOpen}
onModalClose={this.onInteractiveImportModalClose} onModalClose={this.onInteractiveImportModalClose}
/> />
<OrganizeMovieModal
isOpen={this.state.isOrganizingMovieModalOpen}
movieIds={selectedMovieIds}
onModalClose={this.onOrganizeMovieModalClose}
/>
</PageContent> </PageContent>
); );
} }
@ -415,15 +581,21 @@ MovieIndex.propTypes = {
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
isRefreshingMovie: PropTypes.bool.isRequired, isRefreshingMovie: PropTypes.bool.isRequired,
isOrganizingMovie: PropTypes.bool.isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired, isRssSyncExecuting: PropTypes.bool.isRequired,
scrollTop: PropTypes.number.isRequired, scrollTop: PropTypes.number.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired, onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired, onFilterSelect: PropTypes.func.isRequired,
onViewSelect: PropTypes.func.isRequired, onViewSelect: PropTypes.func.isRequired,
onRefreshMoviePress: PropTypes.func.isRequired, onRefreshMoviePress: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired, onRssSyncPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired onScroll: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
}; };
export default MovieIndex; export default MovieIndex;

View File

@ -8,7 +8,7 @@ import createCommandExecutingSelector from 'Store/Selectors/createCommandExecuti
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { fetchMovies } from 'Store/Actions/movieActions'; import { fetchMovies } from 'Store/Actions/movieActions';
import scrollPositions from 'Store/scrollPositions'; 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 { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition'; import withScrollPosition from 'Components/withScrollPosition';
@ -42,17 +42,20 @@ function createMapStateToProps() {
createMovieClientSideCollectionItemsSelector('movieIndex'), createMovieClientSideCollectionItemsSelector('movieIndex'),
createCommandExecutingSelector(commandNames.REFRESH_MOVIE), createCommandExecutingSelector(commandNames.REFRESH_MOVIE),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_MOVIE),
createDimensionsSelector(), createDimensionsSelector(),
( (
movies, movies,
isRefreshingMovie, isRefreshingMovie,
isRssSyncExecuting, isRssSyncExecuting,
isOrganizingMovie,
dimensionsState dimensionsState
) => { ) => {
return { return {
...movies, ...movies,
isRefreshingMovie, isRefreshingMovie,
isRssSyncExecuting, isRssSyncExecuting,
isOrganizingMovie,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }
@ -81,6 +84,10 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(setMovieView({ view })); dispatch(setMovieView({ view }));
}, },
dispatchSaveMovieEditor(payload) {
dispatch(saveMovieEditor(payload));
},
onRefreshMoviePress() { onRefreshMoviePress() {
dispatch(executeCommand({ dispatch(executeCommand({
name: commandNames.REFRESH_MOVIE name: commandNames.REFRESH_MOVIE
@ -128,6 +135,10 @@ class MovieIndexConnector extends Component {
}); });
} }
onSaveSelected = (payload) => {
this.props.dispatchSaveMovieEditor(payload);
}
onScroll = ({ scrollTop }) => { onScroll = ({ scrollTop }) => {
this.setState({ this.setState({
scrollTop scrollTop
@ -146,6 +157,7 @@ class MovieIndexConnector extends Component {
scrollTop={this.state.scrollTop} scrollTop={this.state.scrollTop}
onViewSelect={this.onViewSelect} onViewSelect={this.onViewSelect}
onScroll={this.onScroll} onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
/> />
); );
} }
@ -156,7 +168,8 @@ MovieIndexConnector.propTypes = {
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
scrollTop: PropTypes.number.isRequired, scrollTop: PropTypes.number.isRequired,
dispatchFetchMovies: PropTypes.func.isRequired, dispatchFetchMovies: PropTypes.func.isRequired,
dispatchSetMovieView: PropTypes.func.isRequired dispatchSetMovieView: PropTypes.func.isRequired,
dispatchSaveMovieEditor: PropTypes.func.isRequired
}; };
export default withScrollPosition( export default withScrollPosition(

View File

@ -17,6 +17,13 @@ $hoverScale: 1.05;
position: relative; position: relative;
} }
.editorSelect {
position: absolute;
top: 0;
left: 5px;
z-index: 3;
}
.posterContainer { .posterContainer {
position: relative; position: relative;
} }

View File

@ -7,6 +7,7 @@ import fonts from 'Styles/Variables/fonts';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import CheckInput from 'Components/Form/CheckInput';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
@ -65,6 +66,15 @@ class MovieIndexOverview extends Component {
this.setState({ isDeleteMovieModalOpen: false }); this.setState({ isDeleteMovieModalOpen: false });
} }
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
}
// //
// Render // Render
@ -94,6 +104,9 @@ class MovieIndexOverview extends Component {
isSearchingMovie, isSearchingMovie,
onRefreshMoviePress, onRefreshMoviePress,
onSearchPress, onSearchPress,
isMovieEditorActive,
isSelected,
onSelectedChange,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -118,11 +131,15 @@ class MovieIndexOverview extends Component {
<div className={styles.poster}> <div className={styles.poster}>
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
{ {
status === 'ended' && isMovieEditorActive &&
<div <div className={styles.editorSelect}>
className={styles.ended} <CheckInput
title="Ended" className={styles.checkInput}
/> name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
} }
<Link <Link
@ -253,7 +270,10 @@ MovieIndexOverview.propTypes = {
isRefreshingMovie: PropTypes.bool.isRequired, isRefreshingMovie: PropTypes.bool.isRequired,
isSearchingMovie: PropTypes.bool.isRequired, isSearchingMovie: PropTypes.bool.isRequired,
onRefreshMoviePress: PropTypes.func.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; export default MovieIndexOverview;

View File

@ -169,7 +169,10 @@ class MovieIndexOverviews extends Component {
shortDateFormat, shortDateFormat,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
isSmallScreen isSmallScreen,
selectedState,
isMovieEditorActive,
onSelectedChange
} = this.props; } = this.props;
const { const {
@ -201,6 +204,9 @@ class MovieIndexOverviews extends Component {
style={style} style={style}
movieId={movie.id} movieId={movie.id}
qualityProfileId={movie.qualityProfileId} qualityProfileId={movie.qualityProfileId}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/> />
); );
} }
@ -227,7 +233,8 @@ class MovieIndexOverviews extends Component {
items, items,
scrollTop, scrollTop,
isSmallScreen, isSmallScreen,
onScroll onScroll,
selectedState
} = this.props; } = this.props;
const { const {
@ -257,6 +264,7 @@ class MovieIndexOverviews extends Component {
overscanRowCount={2} overscanRowCount={2}
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
onSectionRendered={this.onSectionRendered} onSectionRendered={this.onSectionRendered}
selectedState={selectedState}
/> />
); );
} }
@ -282,7 +290,10 @@ MovieIndexOverviews.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
onRender: PropTypes.func.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; export default MovieIndexOverviews;

View File

@ -85,6 +85,13 @@ $hoverScale: 1.05;
transition: opacity 0; transition: opacity 0;
} }
.editorSelect {
position: absolute;
top: 10px;
left: 10px;
z-index: 3;
}
.action { .action {
composes: button from '~Components/Link/IconButton.css'; composes: button from '~Components/Link/IconButton.css';

View File

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import CheckInput from 'Components/Form/CheckInput';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import Label from 'Components/Label'; import Label from 'Components/Label';
import Link from 'Components/Link/Link'; 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 // Render
@ -89,6 +99,9 @@ class MovieIndexPoster extends Component {
isSearchingMovie, isSearchingMovie,
onRefreshMoviePress, onRefreshMoviePress,
onSearchPress, onSearchPress,
isMovieEditorActive,
isSelected,
onSelectedChange,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -109,6 +122,17 @@ class MovieIndexPoster extends Component {
<div className={styles.container} style={style}> <div className={styles.container} style={style}>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.posterContainer}> <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}> <Label className={styles.controls}>
<SpinnerIconButton <SpinnerIconButton
className={styles.action} className={styles.action}
@ -249,7 +273,10 @@ MovieIndexPoster.propTypes = {
isRefreshingMovie: PropTypes.bool.isRequired, isRefreshingMovie: PropTypes.bool.isRequired,
isSearchingMovie: PropTypes.bool.isRequired, isSearchingMovie: PropTypes.bool.isRequired,
onRefreshMoviePress: PropTypes.func.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 = { MovieIndexPoster.defaultProps = {

View File

@ -195,7 +195,10 @@ class MovieIndexPosters extends Component {
posterOptions, posterOptions,
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
timeFormat timeFormat,
selectedState,
isMovieEditorActive,
onSelectedChange
} = this.props; } = this.props;
const { const {
@ -234,6 +237,9 @@ class MovieIndexPosters extends Component {
style={style} style={style}
movieId={movie.id} movieId={movie.id}
qualityProfileId={movie.qualityProfileId} qualityProfileId={movie.qualityProfileId}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/> />
); );
} }
@ -260,7 +266,8 @@ class MovieIndexPosters extends Component {
items, items,
scrollTop, scrollTop,
isSmallScreen, isSmallScreen,
onScroll onScroll,
selectedState
} = this.props; } = this.props;
const { const {
@ -294,6 +301,7 @@ class MovieIndexPosters extends Component {
overscanRowCount={2} overscanRowCount={2}
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
onSectionRendered={this.onSectionRendered} onSectionRendered={this.onSectionRendered}
selectedState={selectedState}
/> />
); );
} }
@ -318,7 +326,10 @@ MovieIndexPosters.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
onRender: PropTypes.func.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; export default MovieIndexPosters;

View File

@ -144,7 +144,7 @@ class MovieIndexPosterOptionsModalContent extends Component {
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="showTitle" name="showTitle"
value={showTitle} value={showTitle}
helpText="Show series title under poster" helpText="Show movie title under poster"
onChange={this.onChangePosterOption} onChange={this.onChangePosterOption}
/> />
</FormGroup> </FormGroup>

View File

@ -4,6 +4,7 @@ import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal'; import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
import MovieIndexTableOptionsConnector from './MovieIndexTableOptionsConnector'; import MovieIndexTableOptionsConnector from './MovieIndexTableOptionsConnector';
import styles from './MovieIndexHeader.css'; import styles from './MovieIndexHeader.css';
@ -39,6 +40,10 @@ class MovieIndexHeader extends Component {
const { const {
columns, columns,
onTableOptionChange, onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
isMovieEditorActive,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -57,6 +62,17 @@ class MovieIndexHeader extends Component {
return null; return null;
} }
if (isMovieEditorActive && name === 'select') {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
if (name === 'actions') { if (name === 'actions') {
return ( return (
<VirtualTableHeaderCell <VirtualTableHeaderCell
@ -102,7 +118,11 @@ class MovieIndexHeader extends Component {
MovieIndexHeader.propTypes = { MovieIndexHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired, 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; export default MovieIndexHeader;

View File

@ -15,6 +15,7 @@ import MovieTitleLink from 'Movie/MovieTitleLink';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieStatusCell from './MovieStatusCell'; import MovieStatusCell from './MovieStatusCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import styles from './MovieIndexRow.css'; import styles from './MovieIndexRow.css';
class MovieIndexRow extends Component { class MovieIndexRow extends Component {
@ -80,8 +81,11 @@ class MovieIndexRow extends Component {
columns, columns,
isRefreshingMovie, isRefreshingMovie,
isSearchingMovie, isSearchingMovie,
isMovieEditorActive,
isSelected,
onRefreshMoviePress, onRefreshMoviePress,
onSearchPress onSearchPress,
onSelectedChange
} = this.props; } = this.props;
const { const {
@ -102,6 +106,19 @@ class MovieIndexRow extends Component {
return null; return null;
} }
if (isMovieEditorActive && name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={id}
key={name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'status') { if (name === 'status') {
return ( return (
<MovieStatusCell <MovieStatusCell
@ -322,7 +339,10 @@ MovieIndexRow.propTypes = {
isRefreshingMovie: PropTypes.bool.isRequired, isRefreshingMovie: PropTypes.bool.isRequired,
isSearchingMovie: PropTypes.bool.isRequired, isSearchingMovie: PropTypes.bool.isRequired,
onRefreshMoviePress: PropTypes.func.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 = { MovieIndexRow.defaultProps = {

View File

@ -22,10 +22,13 @@ class MovieIndexTable extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const {
items
} = this.props;
const jumpToCharacter = this.props.jumpToCharacter; const jumpToCharacter = this.props.jumpToCharacter;
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const items = this.props.items;
const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter); const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter);
@ -43,7 +46,10 @@ class MovieIndexTable extends Component {
rowRenderer = ({ key, rowIndex, style }) => { rowRenderer = ({ key, rowIndex, style }) => {
const { const {
items, items,
columns columns,
selectedState,
onSelectedChange,
isMovieEditorActive
} = this.props; } = this.props;
const movie = items[rowIndex]; const movie = items[rowIndex];
@ -56,6 +62,9 @@ class MovieIndexTable extends Component {
columns={columns} columns={columns}
movieId={movie.id} movieId={movie.id}
qualityProfileId={movie.qualityProfileId} qualityProfileId={movie.qualityProfileId}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/> />
); );
} }
@ -75,7 +84,12 @@ class MovieIndexTable extends Component {
contentBody, contentBody,
onSortPress, onSortPress,
onRender, onRender,
onScroll onScroll,
allSelected,
allUnselected,
onSelectAllChange,
isMovieEditorActive,
selectedState
} = this.props; } = this.props;
return ( return (
@ -95,8 +109,13 @@ class MovieIndexTable extends Component {
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
onSortPress={onSortPress} onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
isMovieEditorActive={isMovieEditorActive}
/> />
} }
selectedState={selectedState}
columns={columns} columns={columns}
filters={filters} filters={filters}
sortKey={sortKey} sortKey={sortKey}
@ -120,7 +139,13 @@ MovieIndexTable.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
onRender: 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; export default MovieIndexTable;

View File

@ -27,7 +27,7 @@ function MovieStatusCell(props) {
<Icon <Icon
className={styles.statusIcon} 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'} title={status === 'ended' ? 'Ended' : 'Continuing'}
/> />

View File

@ -7,16 +7,16 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import styles from './MoveSeriesModal.css'; import styles from './MoveMovieModal.css';
function MoveSeriesModal(props) { function MoveMovieModal(props) {
const { const {
originalPath, originalPath,
destinationPath, destinationPath,
destinationRootFolder, destinationRootFolder,
isOpen, isOpen,
onSavePress, onSavePress,
onMoveSeriesPress onMoveMoviePress
} = props; } = props;
if ( if (
@ -46,8 +46,8 @@ function MoveSeriesModal(props) {
<ModalBody> <ModalBody>
{ {
destinationRootFolder ? destinationRootFolder ?
`Would you like to move the series folders to '${destinationRootFolder}'?` : `Would you like to move the movie folders to '${destinationRootFolder}'?` :
`Would you like to move the series files from '${originalPath}' to '${destinationPath}'?` `Would you like to move the movie files from '${originalPath}' to '${destinationPath}'?`
} }
</ModalBody> </ModalBody>
@ -61,7 +61,7 @@ function MoveSeriesModal(props) {
<Button <Button
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onMoveSeriesPress} onPress={onMoveMoviePress}
> >
Yes, Move the Files Yes, Move the Files
</Button> </Button>
@ -71,13 +71,13 @@ function MoveSeriesModal(props) {
); );
} }
MoveSeriesModal.propTypes = { MoveMovieModal.propTypes = {
originalPath: PropTypes.string, originalPath: PropTypes.string,
destinationPath: PropTypes.string, destinationPath: PropTypes.string,
destinationRootFolder: PropTypes.string, destinationRootFolder: PropTypes.string,
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onMoveSeriesPress: PropTypes.func.isRequired onMoveMoviePress: PropTypes.func.isRequired
}; };
export default MoveSeriesModal; export default MoveMovieModal;

View File

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

View File

@ -1,28 +1,28 @@
.title { .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, .quality,
.language { .language {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
} }
.language { .language {
width: 100px; width: 100px;
} }
.rejected, .rejected,
.download { .download {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px; width: 50px;
} }
.age, .age,
.size { .size {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
white-space: nowrap; white-space: nowrap;
} }

View File

@ -8,7 +8,7 @@
} }
.blankpad { .blankpad {
padding-left:2em;
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
padding-left: 2em;
} }

View File

@ -3,7 +3,7 @@
font-weight: bold; font-weight: bold;
} }
.episodeFormat { .standardMovieFormat {
margin-left: 5px; margin-left: 5px;
font-family: $monoSpaceFontFamily; font-family: $monoSpaceFontFamily;
} }

View File

@ -75,7 +75,7 @@ class OrganizePreviewModalContent extends Component {
error, error,
items, items,
renameEpisodes, renameEpisodes,
episodeFormat, standardMovieFormat,
path, path,
onModalClose onModalClose
} = this.props; } = this.props;
@ -129,8 +129,8 @@ class OrganizePreviewModalContent extends Component {
<div> <div>
Naming pattern: Naming pattern:
<span className={styles.episodeFormat}> <span className={styles.standardMovieFormat}>
{episodeFormat} {standardMovieFormat}
</span> </span>
</div> </div>
</Alert> </Alert>
@ -140,11 +140,11 @@ class OrganizePreviewModalContent extends Component {
items.map((item) => { items.map((item) => {
return ( return (
<OrganizePreviewRow <OrganizePreviewRow
key={item.episodeFileId} key={item.movieFileId}
id={item.episodeFileId} id={item.movieFileId}
existingPath={item.existingPath} existingPath={item.existingPath}
newPath={item.newPath} newPath={item.newPath}
isSelected={selectedState[item.episodeFileId]} isSelected={selectedState[item.movieFileId]}
onSelectedChange={this.onSelectedChange} onSelectedChange={this.onSelectedChange}
/> />
); );
@ -190,10 +190,9 @@ OrganizePreviewModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
seasonNumber: PropTypes.string.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
renameEpisodes: PropTypes.bool, renameEpisodes: PropTypes.bool,
episodeFormat: PropTypes.string, standardMovieFormat: PropTypes.string,
onOrganizePress: PropTypes.func.isRequired, onOrganizePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View File

@ -14,14 +14,14 @@ function createMapStateToProps() {
(state) => state.organizePreview, (state) => state.organizePreview,
(state) => state.settings.naming, (state) => state.settings.naming,
createMovieSelector(), createMovieSelector(),
(organizePreview, naming, series) => { (organizePreview, naming, movie) => {
const props = { ...organizePreview }; const props = { ...organizePreview };
props.isFetching = organizePreview.isFetching || naming.isFetching; props.isFetching = organizePreview.isFetching || naming.isFetching;
props.isPopulated = organizePreview.isPopulated && naming.isPopulated; props.isPopulated = organizePreview.isPopulated && naming.isPopulated;
props.error = organizePreview.error || naming.error; props.error = organizePreview.error || naming.error;
props.renameEpisodes = naming.item.renameEpisodes; props.renameEpisodes = naming.item.renameEpisodes;
props.episodeFormat = naming.item.episodeFormat; props.standardMovieFormat = naming.item.standardMovieFormat;
props.path = series.path; props.path = movie.path;
return props; return props;
} }
@ -41,13 +41,11 @@ class OrganizePreviewModalContentConnector extends Component {
componentDidMount() { componentDidMount() {
const { const {
seriesId, movieId
seasonNumber
} = this.props; } = this.props;
this.props.fetchOrganizePreview({ this.props.fetchOrganizePreview({
seriesId, movieId
seasonNumber
}); });
this.props.fetchNamingSettings(); this.props.fetchNamingSettings();
@ -59,7 +57,7 @@ class OrganizePreviewModalContentConnector extends Component {
onOrganizePress = (files) => { onOrganizePress = (files) => {
this.props.executeCommand({ this.props.executeCommand({
name: commandNames.RENAME_FILES, name: commandNames.RENAME_FILES,
seriesId: this.props.seriesId, movieId: this.props.movieId,
files files
}); });
@ -80,8 +78,7 @@ class OrganizePreviewModalContentConnector extends Component {
} }
OrganizePreviewModalContentConnector.propTypes = { OrganizePreviewModalContentConnector.propTypes = {
seriesId: PropTypes.number.isRequired, movieId: PropTypes.number.isRequired,
seasonNumber: PropTypes.number,
fetchOrganizePreview: PropTypes.func.isRequired, fetchOrganizePreview: PropTypes.func.isRequired,
fetchNamingSettings: PropTypes.func.isRequired, fetchNamingSettings: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired, executeCommand: PropTypes.func.isRequired,

View File

@ -76,7 +76,7 @@ function EditRestrictionModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" 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} {...tags}
onChange={onInputChange} onChange={onInputChange}
/> />

View File

@ -326,7 +326,7 @@ class MediaManagement extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
name="folderChmod" name="folderChmod"
helpText="Octal, applied to series/season folders created by Radarr" helpText="Octal, applied to movie folders created by Radarr"
values={fileDateOptions} values={fileDateOptions}
onChange={onInputChange} onChange={onInputChange}
{...settings.folderChmod} {...settings.folderChmod}

View File

@ -98,7 +98,7 @@ function EditNotificationModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="onGrab" 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} isDisabled={!supportsOnGrab.value}
{...onGrab} {...onGrab}
onChange={onInputChange} onChange={onInputChange}
@ -111,7 +111,7 @@ function EditNotificationModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="onDownload" name="onDownload"
helpText="Be notified when episodes are successfully imported" helpText="Be notified when movies are successfully imported"
isDisabled={!supportsOnDownload.value} isDisabled={!supportsOnDownload.value}
{...onDownload} {...onDownload}
onChange={onInputChange} onChange={onInputChange}
@ -126,7 +126,7 @@ function EditNotificationModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="onUpgrade" 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} isDisabled={!supportsOnUpgrade.value}
{...onUpgrade} {...onUpgrade}
onChange={onInputChange} onChange={onInputChange}
@ -140,7 +140,7 @@ function EditNotificationModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="onRename" name="onRename"
helpText="Be notified when episodes are renamed" helpText="Be notified when movies are renamed"
isDisabled={!supportsOnRename.value} isDisabled={!supportsOnRename.value}
{...onRename} {...onRename}
onChange={onInputChange} onChange={onInputChange}
@ -153,7 +153,7 @@ function EditNotificationModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" 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} {...tags}
onChange={onInputChange} onChange={onInputChange}
/> />

View File

@ -110,7 +110,7 @@ function EditDelayProfileModalContent(props) {
{ {
id === 1 ? id === 1 ?
<Alert> <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> : </Alert> :
<FormGroup> <FormGroup>
@ -120,7 +120,7 @@ function EditDelayProfileModalContent(props) {
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
{...tags} {...tags}
helpText="Applies to series with at least one matching tag" helpText="Applies to movies with at least one matching tag"
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>

View File

@ -109,7 +109,7 @@ function Settings() {
</Link> </Link>
<div className={styles.summary}> <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> </div>
<Link <Link

View File

@ -190,8 +190,8 @@ export const actionHandlers = handleThunks({
const item = _.find(items, { id }); const item = _.find(items, { id });
const selectedMovie = item.selectedMovie; const selectedMovie = item.selectedMovie;
// Make sure we have a selected series and // Make sure we have a selected movie and
// the same series hasn't been added yet. // the same movie hasn't been added yet.
if (selectedMovie && !_.some(acc, { tmdbId: selectedMovie.tmdbId })) { if (selectedMovie && !_.some(acc, { tmdbId: selectedMovie.tmdbId })) {
const newMovie = getNewMovie(_.cloneDeep(selectedMovie), item); const newMovie = getNewMovie(_.cloneDeep(selectedMovie), item);
newMovie.path = item.path; newMovie.path = item.path;

View File

@ -17,7 +17,6 @@ import * as queue from './queueActions';
import * as releases from './releaseActions'; import * as releases from './releaseActions';
import * as rootFolders from './rootFolderActions'; import * as rootFolders from './rootFolderActions';
import * as movies from './movieActions'; import * as movies from './movieActions';
import * as movieEditor from './movieEditorActions';
import * as movieHistory from './movieHistoryActions'; import * as movieHistory from './movieHistoryActions';
import * as movieIndex from './movieIndexActions'; import * as movieIndex from './movieIndexActions';
import * as settings from './settingsActions'; import * as settings from './settingsActions';
@ -44,7 +43,6 @@ export default [
releases, releases,
rootFolders, rootFolders,
movies, movies,
movieEditor,
movieHistory, movieHistory,
movieIndex, movieIndex,
settings, settings,

View File

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

View File

@ -1,10 +1,14 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import sortByName from 'Utilities/Array/sortByName'; import sortByName from 'Utilities/Array/sortByName';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import { createThunk, handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import { set, updateItem } from './baseActions';
import { filters, filterPredicates, sortPredicates } from './movieActions'; import { filters, filterPredicates, sortPredicates } from './movieActions';
// //
// Variables // Variables
@ -15,6 +19,10 @@ export const section = 'movieIndex';
// State // State
export const defaultState = { export const defaultState = {
isSaving: false,
saveError: null,
isDeleting: false,
deleteError: null,
sortKey: 'sortTitle', sortKey: 'sortTitle',
sortDirection: sortDirections.ASCENDING, sortDirection: sortDirections.ASCENDING,
secondarySortKey: 'sortTitle', secondarySortKey: 'sortTitle',
@ -47,6 +55,13 @@ export const defaultState = {
}, },
columns: [ columns: [
{
name: 'select',
columnLabel: 'select',
isSortable: false,
isVisible: true,
isModifiable: false
},
{ {
name: 'status', name: 'status',
columnLabel: 'Status', columnLabel: 'Status',
@ -214,8 +229,8 @@ export const defaultState = {
label: 'Genres', label: 'Genres',
type: filterBuilderTypes.ARRAY, type: filterBuilderTypes.ARRAY,
optionsSelector: function(items) { optionsSelector: function(items) {
const tagList = items.reduce((acc, series) => { const tagList = items.reduce((acc, movie) => {
series.genres.forEach((genre) => { movie.genres.forEach((genre) => {
acc.push({ acc.push({
id: genre, id: genre,
name: 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_TABLE_OPTION = 'movieIndex/setMovieTableOption';
export const SET_MOVIE_POSTER_OPTION = 'movieIndex/setMoviePosterOption'; export const SET_MOVIE_POSTER_OPTION = 'movieIndex/setMoviePosterOption';
export const SET_MOVIE_OVERVIEW_OPTION = 'movieIndex/setMovieOverviewOption'; export const SET_MOVIE_OVERVIEW_OPTION = 'movieIndex/setMovieOverviewOption';
export const SAVE_MOVIE_EDITOR = 'movieIndex/saveMovieEditor';
export const BULK_DELETE_MOVIE = 'movieIndex/bulkDeleteMovie';
// //
// Action Creators // Action Creators
@ -278,6 +295,85 @@ export const setMovieView = createAction(SET_MOVIE_VIEW);
export const setMovieTableOption = createAction(SET_MOVIE_TABLE_OPTION); export const setMovieTableOption = createAction(SET_MOVIE_TABLE_OPTION);
export const setMoviePosterOption = createAction(SET_MOVIE_POSTER_OPTION); export const setMoviePosterOption = createAction(SET_MOVIE_POSTER_OPTION);
export const setMovieOverviewOption = createAction(SET_MOVIE_OVERVIEW_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 // Reducers

View File

@ -8,10 +8,10 @@ function createImportMovieItemSelector() {
(state) => state.addMovie, (state) => state.addMovie,
(state) => state.importMovie, (state) => state.importMovie,
createAllMoviesSelector(), createAllMoviesSelector(),
(id, addMovie, importMovie, series) => { (id, addMovie, importMovie, movies) => {
const item = _.find(importMovie.items, { id }) || {}; const item = _.find(importMovie.items, { id }) || {};
const selectedMovie = item && item.selectedMovie; const selectedMovie = item && item.selectedMovie;
const isExistingMovie = !!selectedMovie && _.some(series, { tvdbId: selectedMovie.tvdbId }); const isExistingMovie = !!selectedMovie && _.some(movies, { tmdbId: selectedMovie.tmdbId });
return { return {
defaultMonitor: addMovie.defaults.monitor, defaultMonitor: addMovie.defaults.monitor,

View File

@ -46,7 +46,7 @@ module.exports = {
// Modal // Modal
modalBodyPadding: '30px', modalBodyPadding: '30px',
// Series // Movie
movieIndexColumnPadding: '20px', movieIndexColumnPadding: '20px',
movieIndexColumnPaddingSmallScreen: '10px', movieIndexColumnPaddingSmallScreen: '10px',
movieIndexOverviewInfoRowHeight: '21px' movieIndexOverviewInfoRowHeight: '21px'

View 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();
}
}
}

View File

@ -220,6 +220,7 @@
<Compile Include="Movies\AlternativeTitles\AlternativeTitle.cs" /> <Compile Include="Movies\AlternativeTitles\AlternativeTitle.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitleRepository.cs" /> <Compile Include="Movies\AlternativeTitles\AlternativeTitleRepository.cs" />
<Compile Include="Movies\AlternativeTitles\AlternativeTitleService.cs" /> <Compile Include="Movies\AlternativeTitles\AlternativeTitleService.cs" />
<Compile Include="Movies\Commands\BulkMoveMovieCommand.cs" />
<Compile Include="Movies\Events\MoviesImportedEvent.cs" /> <Compile Include="Movies\Events\MoviesImportedEvent.cs" />
<Compile Include="NetImport\NetImportListLevels.cs" /> <Compile Include="NetImport\NetImportListLevels.cs" />
<Compile Include="NetImport\TMDb\TMDbLanguageCodes.cs" /> <Compile Include="NetImport\TMDb\TMDbLanguageCodes.cs" />

View File

@ -7,58 +7,98 @@
using Radarr.Http.REST; using Radarr.Http.REST;
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;
using Radarr.Http.Mapping; using Radarr.Http.Mapping;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Movies.Commands;
using NzbDrone.Core.Messaging.Commands;
namespace Radarr.Api.V2.Movies namespace Radarr.Api.V2.Movies
{ {
public class MovieEditorModule : RadarrV2Module public class MovieEditorModule : RadarrV2Module
{ {
private readonly IMovieService _movieService; private readonly IMovieService _movieService;
private readonly IManageCommandQueue _commandQueueManager;
public MovieEditorModule(IMovieService movieService) public MovieEditorModule(IMovieService movieService, IManageCommandQueue commandQueueManager)
: base("/movie/editor") : base("/movie/editor")
{ {
_movieService = movieService; _movieService = movieService;
Put["/"] = Movie => SaveAll(); _commandQueueManager = commandQueueManager;
Put["/delete"] = Movie => DeleteSelected(); Put["/"] = movie => SaveAll();
Delete["/"] = movie => DeleteMovies();
} }
private Response SaveAll() 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() .ToResource()
.AsResponse(HttpStatusCode.Accepted); .AsResponse(HttpStatusCode.Accepted);
} }
private Response DeleteSelected() private Response DeleteMovies()
{ {
var deleteFiles = false; var resource = Request.Body.FromJson<MovieEditorResource>();
var addExclusion = false;
var deleteFilesQuery = Request.Query.deleteFiles;
var addExclusionQuery = Request.Query.addExclusion;
if (deleteFilesQuery.HasValue) foreach (var id in resource.MovieIds)
{ {
deleteFiles = Convert.ToBoolean(deleteFilesQuery.Value); _movieService.DeleteMovie(id, false, false);
}
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);
} }
return new Response return new object().AsResponse();
{
StatusCode = HttpStatusCode.Accepted
};
} }
} }
} }

View 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
}
}

View File

@ -136,6 +136,7 @@
<Compile Include="Movies\AlternativeYearModule.cs" /> <Compile Include="Movies\AlternativeYearModule.cs" />
<Compile Include="Movies\AlternativeYearResource.cs" /> <Compile Include="Movies\AlternativeYearResource.cs" />
<Compile Include="Movies\FetchMovieListModule.cs" /> <Compile Include="Movies\FetchMovieListModule.cs" />
<Compile Include="Movies\MovieEditorResource.cs" />
<Compile Include="Movies\MovieImportModule.cs" /> <Compile Include="Movies\MovieImportModule.cs" />
<Compile Include="Movies\MovieDiscoverModule.cs" /> <Compile Include="Movies\MovieDiscoverModule.cs" />
<Compile Include="Movies\MovieEditorModule.cs" /> <Compile Include="Movies\MovieEditorModule.cs" />

View File

@ -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) 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["Pragma"] = "no-cache";
headers["Expires"] = "0"; headers["Expires"] = "0";

View File

@ -54,5 +54,17 @@ public static bool GetBooleanQueryParameter(this Request request, string paramet
return defaultValue; 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;
}
} }
} }

View File

@ -12,7 +12,7 @@ public abstract class HtmlMapperBase : StaticResourceMapperBase
{ {
private readonly IDiskProvider _diskProvider; private readonly IDiskProvider _diskProvider;
private readonly Func<ICacheBreakerProvider> _cacheBreakProviderFactory; 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; private string _generatedContent;