diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 82dcc9341b..3203fae548 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -9,6 +9,7 @@ import MovieCreditAppState from './MovieCreditAppState'; import MovieFilesAppState from './MovieFilesAppState'; import MovieHistoryAppState from './MovieHistoryAppState'; import MoviesAppState, { MovieIndexAppState } from './MoviesAppState'; +import OrganizePreviewAppState from './OrganizePreviewAppState'; import ParseAppState from './ParseAppState'; import PathsAppState from './PathsAppState'; import QueueAppState from './QueueAppState'; @@ -76,6 +77,7 @@ interface AppState { movieHistory: MovieHistoryAppState; movieIndex: MovieIndexAppState; movies: MoviesAppState; + organizePreview: OrganizePreviewAppState; parse: ParseAppState; paths: PathsAppState; queue: QueueAppState; diff --git a/frontend/src/App/State/OrganizePreviewAppState.ts b/frontend/src/App/State/OrganizePreviewAppState.ts new file mode 100644 index 0000000000..b8b907852b --- /dev/null +++ b/frontend/src/App/State/OrganizePreviewAppState.ts @@ -0,0 +1,13 @@ +import ModelBase from 'App/ModelBase'; +import AppSectionState from 'App/State/AppSectionState'; + +export interface OrganizePreviewModel extends ModelBase { + movieId: number; + movieFileId: number; + existingPath: string; + newPath: string; +} + +type OrganizePreviewAppState = AppSectionState; + +export default OrganizePreviewAppState; diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index 077f8cb65b..6b23dee7e3 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -33,7 +33,7 @@ import MoviePoster from 'Movie/MoviePoster'; import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal'; import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable'; import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable'; -import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; +import OrganizePreviewModal from 'Organize/OrganizePreviewModal'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import fonts from 'Styles/Variables/fonts'; import * as keyCodes from 'Utilities/Constants/keyCodes'; @@ -724,7 +724,7 @@ class MovieDetails extends Component { - - { - isOpen && - - } - - ); -} - -OrganizePreviewModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default OrganizePreviewModal; diff --git a/frontend/src/Organize/OrganizePreviewModal.tsx b/frontend/src/Organize/OrganizePreviewModal.tsx new file mode 100644 index 0000000000..4160a5885b --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModal.tsx @@ -0,0 +1,37 @@ +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import Modal from 'Components/Modal/Modal'; +import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions'; +import OrganizePreviewModalContent, { + OrganizePreviewModalContentProps, +} from './OrganizePreviewModalContent'; + +interface OrganizePreviewModalProps extends OrganizePreviewModalContentProps { + isOpen: boolean; + onModalClose: () => void; +} + +function OrganizePreviewModal({ + isOpen, + onModalClose, + ...otherProps +}: OrganizePreviewModalProps) { + const dispatch = useDispatch(); + + const handleOnModalClose = useCallback(() => { + dispatch(clearOrganizePreview()); + onModalClose(); + }, [dispatch, onModalClose]); + + return ( + + {isOpen ? ( + + ) : null} + + ); +} +export default OrganizePreviewModal; diff --git a/frontend/src/Organize/OrganizePreviewModalConnector.js b/frontend/src/Organize/OrganizePreviewModalConnector.js deleted file mode 100644 index 4abcaf8422..0000000000 --- a/frontend/src/Organize/OrganizePreviewModalConnector.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions'; -import OrganizePreviewModal from './OrganizePreviewModal'; - -const mapDispatchToProps = { - clearOrganizePreview -}; - -class OrganizePreviewModalConnector extends Component { - - // - // Listeners - - onModalClose = () => { - this.props.clearOrganizePreview(); - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -OrganizePreviewModalConnector.propTypes = { - clearOrganizePreview: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(undefined, mapDispatchToProps)(OrganizePreviewModalConnector); diff --git a/frontend/src/Organize/OrganizePreviewModalContent.js b/frontend/src/Organize/OrganizePreviewModalContent.js deleted file mode 100644 index 33ee8baa6c..0000000000 --- a/frontend/src/Organize/OrganizePreviewModalContent.js +++ /dev/null @@ -1,196 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import CheckInput from 'Components/Form/CheckInput'; -import Button from 'Components/Link/Button'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import ModalBody from 'Components/Modal/ModalBody'; -import ModalContent from 'Components/Modal/ModalContent'; -import ModalFooter from 'Components/Modal/ModalFooter'; -import ModalHeader from 'Components/Modal/ModalHeader'; -import { kinds } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; -import selectAll from 'Utilities/Table/selectAll'; -import toggleSelected from 'Utilities/Table/toggleSelected'; -import OrganizePreviewRow from './OrganizePreviewRow'; -import styles from './OrganizePreviewModalContent.css'; - -function getValue(allSelected, allUnselected) { - if (allSelected) { - return true; - } else if (allUnselected) { - return false; - } - - return null; -} - -class OrganizePreviewModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - allSelected: false, - allUnselected: false, - lastToggled: null, - selectedState: {} - }; - } - - // - // 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); - }); - }; - - onOrganizePress = () => { - this.props.onOrganizePress(this.getSelectedIds()); - }; - - // - // Render - - render() { - const { - isFetching, - isPopulated, - error, - items, - renameMovies, - standardMovieFormat, - path, - onModalClose - } = this.props; - - const { - allSelected, - allUnselected, - selectedState - } = this.state; - - const selectAllValue = getValue(allSelected, allUnselected); - - return ( - - - {translate('OrganizeModalHeader')} - - - - { - isFetching && - - } - - { - !isFetching && error && - {translate('OrganizeLoadError')} - } - - { - !isFetching && isPopulated && !items.length && -
- { - renameMovies ? -
{translate('OrganizeNothingToRename')}
: -
{translate('OrganizeRenamingDisabled')}
- } -
- } - - { - !isFetching && isPopulated && !!items.length && -
- -
- -
- -
- -
-
- -
- { - items.map((item) => { - return ( - - ); - }) - } -
-
- } -
- - - { - isPopulated && !!items.length && - - } - - - - - -
- ); - } -} - -OrganizePreviewModalContent.propTypes = { - isFetching: PropTypes.bool.isRequired, - isPopulated: PropTypes.bool.isRequired, - error: PropTypes.object, - items: PropTypes.arrayOf(PropTypes.object).isRequired, - path: PropTypes.string.isRequired, - renameMovies: PropTypes.bool, - standardMovieFormat: PropTypes.string, - onOrganizePress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default OrganizePreviewModalContent; diff --git a/frontend/src/Organize/OrganizePreviewModalContent.tsx b/frontend/src/Organize/OrganizePreviewModalContent.tsx new file mode 100644 index 0000000000..009345c6d4 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalContent.tsx @@ -0,0 +1,193 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import AppState from 'App/State/AppState'; +import * as commandNames from 'Commands/commandNames'; +import Alert from 'Components/Alert'; +import CheckInput from 'Components/Form/CheckInput'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import useSelectState from 'Helpers/Hooks/useSelectState'; +import { kinds } from 'Helpers/Props'; +import useMovie from 'Movie/useMovie'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions'; +import { fetchNamingSettings } from 'Store/Actions/settingsActions'; +import { CheckInputChanged } from 'typings/inputs'; +import { SelectStateInputProps } from 'typings/props'; +import translate from 'Utilities/String/translate'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import OrganizePreviewRow from './OrganizePreviewRow'; +import styles from './OrganizePreviewModalContent.css'; + +function getValue(allSelected: boolean, allUnselected: boolean) { + if (allSelected) { + return true; + } else if (allUnselected) { + return false; + } + + return null; +} + +export interface OrganizePreviewModalContentProps { + movieId: number; + onModalClose: () => void; +} + +function OrganizePreviewModalContent({ + movieId, + onModalClose, +}: OrganizePreviewModalContentProps) { + const dispatch = useDispatch(); + const { + items, + isFetching: isPreviewFetching, + isPopulated: isPreviewPopulated, + error: previewError, + } = useSelector((state: AppState) => state.organizePreview); + + const { + isFetching: isNamingFetching, + isPopulated: isNamingPopulated, + error: namingError, + item: naming, + } = useSelector((state: AppState) => state.settings.naming); + + const movie = useMovie(movieId)!; + const [selectState, setSelectState] = useSelectState(); + + const { allSelected, allUnselected, selectedState } = selectState; + const isFetching = isPreviewFetching || isNamingFetching; + const isPopulated = isPreviewPopulated && isNamingPopulated; + const error = previewError || namingError; + const { renameMovies, standardMovieFormat } = naming; + + const selectAllValue = getValue(allSelected, allUnselected); + + const handleSelectAllChange = useCallback( + ({ value }: CheckInputChanged) => { + setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + }, + [items, setSelectState] + ); + + const handleSelectedChange = useCallback( + ({ id, value, shiftKey = false }: SelectStateInputProps) => { + setSelectState({ + type: 'toggleSelected', + items, + id, + isSelected: value, + shiftKey, + }); + }, + [items, setSelectState] + ); + + const handleOrganizePress = useCallback(() => { + const files = getSelectedIds(selectedState); + + dispatch( + executeCommand({ + name: commandNames.RENAME_FILES, + files, + movieId, + }) + ); + + onModalClose(); + }, [movieId, selectedState, dispatch, onModalClose]); + + useEffect(() => { + dispatch(fetchOrganizePreview({ movieId })); + dispatch(fetchNamingSettings()); + }, [movieId, dispatch]); + + return ( + + {translate('OrganizeModalHeader')} + + + {isFetching ? : null} + + {!isFetching && error ? ( + {translate('OrganizeLoadError')} + ) : null} + + {!isFetching && isPopulated && !items.length ? ( +
+ {renameMovies ? ( +
{translate('OrganizeNothingToRename')}
+ ) : ( +
{translate('OrganizeRenamingDisabled')}
+ )} +
+ ) : null} + + {!isFetching && isPopulated && items.length ? ( +
+ +
+ +
+ +
+ +
+
+ +
+ {items.map((item) => { + return ( + + ); + })} +
+
+ ) : null} +
+ + + {isPopulated && items.length ? ( + + ) : null} + + + + + +
+ ); +} + +export default OrganizePreviewModalContent; diff --git a/frontend/src/Organize/OrganizePreviewModalContentConnector.js b/frontend/src/Organize/OrganizePreviewModalContentConnector.js deleted file mode 100644 index b171f69162..0000000000 --- a/frontend/src/Organize/OrganizePreviewModalContentConnector.js +++ /dev/null @@ -1,88 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import * as commandNames from 'Commands/commandNames'; -import { executeCommand } from 'Store/Actions/commandActions'; -import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions'; -import { fetchNamingSettings } from 'Store/Actions/settingsActions'; -import createMovieSelector from 'Store/Selectors/createMovieSelector'; -import OrganizePreviewModalContent from './OrganizePreviewModalContent'; - -function createMapStateToProps() { - return createSelector( - (state) => state.organizePreview, - (state) => state.settings.naming, - createMovieSelector(), - (organizePreview, naming, movie) => { - const props = { ...organizePreview }; - props.isFetching = organizePreview.isFetching || naming.isFetching; - props.isPopulated = organizePreview.isPopulated && naming.isPopulated; - props.error = organizePreview.error || naming.error; - props.renameMovies = naming.item.renameMovies; - props.standardMovieFormat = naming.item.standardMovieFormat; - props.path = movie.path; - - return props; - } - ); -} - -const mapDispatchToProps = { - fetchOrganizePreview, - fetchNamingSettings, - executeCommand -}; - -class OrganizePreviewModalContentConnector extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - movieId - } = this.props; - - this.props.fetchOrganizePreview({ - movieId - }); - - this.props.fetchNamingSettings(); - } - - // - // Listeners - - onOrganizePress = (files) => { - this.props.executeCommand({ - name: commandNames.RENAME_FILES, - movieId: this.props.movieId, - files - }); - - this.props.onModalClose(); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -OrganizePreviewModalContentConnector.propTypes = { - movieId: PropTypes.number.isRequired, - fetchOrganizePreview: PropTypes.func.isRequired, - fetchNamingSettings: PropTypes.func.isRequired, - executeCommand: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(OrganizePreviewModalContentConnector); diff --git a/frontend/src/Organize/OrganizePreviewRow.js b/frontend/src/Organize/OrganizePreviewRow.js deleted file mode 100644 index 00040ba3ed..0000000000 --- a/frontend/src/Organize/OrganizePreviewRow.js +++ /dev/null @@ -1,90 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import CheckInput from 'Components/Form/CheckInput'; -import Icon from 'Components/Icon'; -import { icons, kinds } from 'Helpers/Props'; -import styles from './OrganizePreviewRow.css'; - -class OrganizePreviewRow extends Component { - - // - // Lifecycle - - componentDidMount() { - const { - id, - onSelectedChange - } = this.props; - - onSelectedChange({ id, value: true }); - } - - // - // Listeners - - onSelectedChange = ({ value, shiftKey }) => { - const { - id, - onSelectedChange - } = this.props; - - onSelectedChange({ id, value, shiftKey }); - }; - - // - // Render - - render() { - const { - id, - existingPath, - newPath, - isSelected - } = this.props; - - return ( -
- - -
-
- - - - {existingPath} - -
- -
- - - - {newPath} - -
-
-
- ); - } -} - -OrganizePreviewRow.propTypes = { - id: PropTypes.number.isRequired, - existingPath: PropTypes.string.isRequired, - newPath: PropTypes.string.isRequired, - isSelected: PropTypes.bool, - onSelectedChange: PropTypes.func.isRequired -}; - -export default OrganizePreviewRow; diff --git a/frontend/src/Organize/OrganizePreviewRow.tsx b/frontend/src/Organize/OrganizePreviewRow.tsx new file mode 100644 index 0000000000..398ea31ea1 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewRow.tsx @@ -0,0 +1,61 @@ +import React, { useCallback, useEffect } from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import Icon from 'Components/Icon'; +import { icons, kinds } from 'Helpers/Props'; +import { CheckInputChanged } from 'typings/inputs'; +import { SelectStateInputProps } from 'typings/props'; +import styles from './OrganizePreviewRow.css'; + +interface OrganizePreviewRowProps { + id: number; + existingPath: string; + newPath: string; + isSelected?: boolean; + onSelectedChange: (props: SelectStateInputProps) => void; +} + +function OrganizePreviewRow({ + id, + existingPath, + newPath, + isSelected, + onSelectedChange, +}: OrganizePreviewRowProps) { + const handleSelectedChange = useCallback( + ({ value, shiftKey }: CheckInputChanged) => { + onSelectedChange({ id, value, shiftKey }); + }, + [id, onSelectedChange] + ); + + useEffect(() => { + onSelectedChange({ id, value: true, shiftKey: false }); + }, [id, onSelectedChange]); + + return ( +
+ + +
+
+ + + {existingPath} +
+ +
+ + + {newPath} +
+
+
+ ); +} + +export default OrganizePreviewRow; diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts index eb42e316d6..e218abbbd9 100644 --- a/frontend/src/typings/inputs.ts +++ b/frontend/src/typings/inputs.ts @@ -5,4 +5,6 @@ export type InputChanged = { export type InputOnChange = (change: InputChanged) => void; -export type CheckInputChanged = InputChanged; +export interface CheckInputChanged extends InputChanged { + shiftKey: boolean; +}