Convert Preview Rename to TypeScript

(cherry picked from commit a2fd23c84d0a9d01864119d2e643970845c9e49e)
This commit is contained in:
Mark McDowall 2024-12-22 21:46:10 -08:00 committed by Bogdan
parent 049bf7715e
commit e4e96fc7f9
12 changed files with 311 additions and 450 deletions

View file

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

View file

@ -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<OrganizePreviewModel>;
export default OrganizePreviewAppState;

View file

@ -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 {
</FieldSet>
</div>
<OrganizePreviewModalConnector
<OrganizePreviewModal
isOpen={isOrganizeModalOpen}
movieId={id}
onModalClose={this.onOrganizeModalClose}

View file

@ -1,34 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import OrganizePreviewModalContentConnector from './OrganizePreviewModalContentConnector';
function OrganizePreviewModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
{
isOpen &&
<OrganizePreviewModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
}
</Modal>
);
}
OrganizePreviewModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default OrganizePreviewModal;

View file

@ -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 (
<Modal isOpen={isOpen} onModalClose={handleOnModalClose}>
{isOpen ? (
<OrganizePreviewModalContent
{...otherProps}
onModalClose={handleOnModalClose}
/>
) : null}
</Modal>
);
}
export default OrganizePreviewModal;

View file

@ -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 (
<OrganizePreviewModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
OrganizePreviewModalConnector.propTypes = {
clearOrganizePreview: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(OrganizePreviewModalConnector);

View file

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('OrganizeModalHeader')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
}
{
!isFetching && isPopulated && !items.length &&
<div>
{
renameMovies ?
<div>{translate('OrganizeNothingToRename')}</div> :
<div>{translate('OrganizeRenamingDisabled')}</div>
}
</div>
}
{
!isFetching && isPopulated && !!items.length &&
<div>
<Alert>
<div>
<InlineMarkdown data={translate('OrganizeRelativePaths', { path })} blockClassName={styles.path} />
</div>
<div>
<InlineMarkdown data={translate('OrganizeNamingPattern', { standardMovieFormat })} blockClassName={styles.standardMovieFormat} />
</div>
</Alert>
<div className={styles.previews}>
{
items.map((item) => {
return (
<OrganizePreviewRow
key={item.movieFileId}
id={item.movieFileId}
existingPath={item.existingPath}
newPath={item.newPath}
isSelected={selectedState[item.movieFileId]}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</div>
</div>
}
</ModalBody>
<ModalFooter>
{
isPopulated && !!items.length &&
<CheckInput
className={styles.selectAllInput}
containerClassName={styles.selectAllInputContainer}
name="selectAll"
value={selectAllValue}
onChange={this.onSelectAllChange}
/>
}
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={this.onOrganizePress}
>
{translate('Organize')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
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;

View file

@ -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 (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('OrganizeModalHeader')}</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
) : null}
{!isFetching && isPopulated && !items.length ? (
<div>
{renameMovies ? (
<div>{translate('OrganizeNothingToRename')}</div>
) : (
<div>{translate('OrganizeRenamingDisabled')}</div>
)}
</div>
) : null}
{!isFetching && isPopulated && items.length ? (
<div>
<Alert>
<div>
<InlineMarkdown
data={translate('OrganizeRelativePaths', {
path: movie.path,
})}
blockClassName={styles.path}
/>
</div>
<div>
<InlineMarkdown
data={translate('OrganizeNamingPattern', {
standardMovieFormat,
})}
blockClassName={styles.standardMovieFormat}
/>
</div>
</Alert>
<div className={styles.previews}>
{items.map((item) => {
return (
<OrganizePreviewRow
key={item.movieFileId}
id={item.movieFileId}
existingPath={item.existingPath}
newPath={item.newPath}
isSelected={selectedState[item.movieFileId]}
onSelectedChange={handleSelectedChange}
/>
);
})}
</div>
</div>
) : null}
</ModalBody>
<ModalFooter>
{isPopulated && items.length ? (
<CheckInput
className={styles.selectAllInput}
containerClassName={styles.selectAllInputContainer}
name="selectAll"
value={selectAllValue}
onChange={handleSelectAllChange}
/>
) : null}
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.PRIMARY} onPress={handleOrganizePress}>
{translate('Organize')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default OrganizePreviewModalContent;

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 * 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 (
<OrganizePreviewModalContent
{...this.props}
onOrganizePress={this.onOrganizePress}
/>
);
}
}
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);

View file

@ -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 (
<div className={styles.row}>
<CheckInput
containerClassName={styles.selectedContainer}
name={id.toString()}
value={isSelected}
onChange={this.onSelectedChange}
/>
<div>
<div>
<Icon
name={icons.SUBTRACT}
kind={kinds.DANGER}
/>
<span className={styles.path}>
{existingPath}
</span>
</div>
<div>
<Icon
name={icons.ADD}
kind={kinds.SUCCESS}
/>
<span className={styles.path}>
{newPath}
</span>
</div>
</div>
</div>
);
}
}
OrganizePreviewRow.propTypes = {
id: PropTypes.number.isRequired,
existingPath: PropTypes.string.isRequired,
newPath: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
export default OrganizePreviewRow;

View file

@ -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 (
<div className={styles.row}>
<CheckInput
containerClassName={styles.selectedContainer}
name={id.toString()}
value={isSelected}
onChange={handleSelectedChange}
/>
<div>
<div>
<Icon name={icons.SUBTRACT} kind={kinds.DANGER} />
<span className={styles.path}>{existingPath}</span>
</div>
<div>
<Icon name={icons.ADD} kind={kinds.SUCCESS} />
<span className={styles.path}>{newPath}</span>
</div>
</div>
</div>
);
}
export default OrganizePreviewRow;

View file

@ -5,4 +5,6 @@ export type InputChanged<T = unknown> = {
export type InputOnChange<T> = (change: InputChanged<T>) => void;
export type CheckInputChanged = InputChanged<boolean>;
export interface CheckInputChanged extends InputChanged<boolean> {
shiftKey: boolean;
}