diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index cbf9d8de2..6299b498d 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -16,6 +16,8 @@ import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; import QualityProfile from 'typings/QualityProfile'; import General from 'typings/Settings/General'; +import NamingConfig from 'typings/Settings/NamingConfig'; +import NamingExample from 'typings/Settings/NamingExample'; import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import UiSettings from 'typings/Settings/UiSettings'; @@ -30,6 +32,13 @@ export interface GeneralAppState extends AppSectionItemState, AppSectionSaveState {} +export interface NamingAppState + extends AppSectionItemState, + AppSectionSaveState {} + +export interface NamingExamplesAppState + extends AppSectionItemState {} + export interface ImportListAppState extends AppSectionState, AppSectionDeleteState, @@ -88,6 +97,8 @@ interface SettingsAppState { indexerFlags: IndexerFlagSettingsAppState; indexers: IndexerAppState; languages: LanguageSettingsAppState; + naming: NamingAppState; + namingExamples: NamingExamplesAppState; notifications: NotificationAppState; qualityProfiles: QualityProfilesAppState; releaseProfiles: ReleaseProfilesAppState; diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js index e903079b5..bb0e4da2e 100644 --- a/frontend/src/Settings/MediaManagement/MediaManagement.js +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -13,7 +13,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props'; import RootFolders from 'RootFolder/RootFolders'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; -import NamingConnector from './Naming/NamingConnector'; +import Naming from './Naming/Naming'; import AddRootFolder from './RootFolder/AddRootFolder'; const episodeTitleRequiredOptions = [ @@ -127,7 +127,7 @@ class MediaManagement extends Component { /> - + { isFetching ? diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js deleted file mode 100644 index 6fcbdb30e..000000000 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.js +++ /dev/null @@ -1,425 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Alert from 'Components/Alert'; -import FieldSet from 'Components/FieldSet'; -import Form from 'Components/Form/Form'; -import FormGroup from 'Components/Form/FormGroup'; -import FormInputButton from 'Components/Form/FormInputButton'; -import FormInputGroup from 'Components/Form/FormInputGroup'; -import FormLabel from 'Components/Form/FormLabel'; -import LoadingIndicator from 'Components/Loading/LoadingIndicator'; -import { inputTypes, kinds, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import NamingModal from './NamingModal'; -import styles from './Naming.css'; - -class Naming extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - isNamingModalOpen: false, - namingModalOptions: null - }; - } - - // - // Listeners - - onStandardNamingModalOpenClick = () => { - this.setState({ - isNamingModalOpen: true, - namingModalOptions: { - name: 'standardEpisodeFormat', - season: true, - episode: true, - additional: true - } - }); - }; - - onDailyNamingModalOpenClick = () => { - this.setState({ - isNamingModalOpen: true, - namingModalOptions: { - name: 'dailyEpisodeFormat', - season: true, - episode: true, - daily: true, - additional: true - } - }); - }; - - onAnimeNamingModalOpenClick = () => { - this.setState({ - isNamingModalOpen: true, - namingModalOptions: { - name: 'animeEpisodeFormat', - season: true, - episode: true, - anime: true, - additional: true - } - }); - }; - - onSeriesFolderNamingModalOpenClick = () => { - this.setState({ - isNamingModalOpen: true, - namingModalOptions: { - name: 'seriesFolderFormat' - } - }); - }; - - onSeasonFolderNamingModalOpenClick = () => { - this.setState({ - isNamingModalOpen: true, - namingModalOptions: { - name: 'seasonFolderFormat', - season: true - } - }); - }; - - onSpecialsFolderNamingModalOpenClick = () => { - this.setState({ - isNamingModalOpen: true, - namingModalOptions: { - name: 'specialsFolderFormat', - season: true - } - }); - }; - - onNamingModalClose = () => { - this.setState({ isNamingModalOpen: false }); - }; - - // - // Render - - render() { - const { - advancedSettings, - isFetching, - error, - settings, - hasSettings, - examples, - examplesPopulated, - onInputChange - } = this.props; - - const { - isNamingModalOpen, - namingModalOptions - } = this.state; - - const renameEpisodes = hasSettings && settings.renameEpisodes.value; - const replaceIllegalCharacters = hasSettings && settings.replaceIllegalCharacters.value; - - const multiEpisodeStyleOptions = [ - { key: 0, value: translate('Extend'), hint: 'S01E01-02-03' }, - { key: 1, value: translate('Duplicate'), hint: 'S01E01.S01E02' }, - { key: 2, value: translate('Repeat'), hint: 'S01E01E02E03' }, - { key: 3, value: translate('Scene'), hint: 'S01E01-E02-E03' }, - { key: 4, value: translate('Range'), hint: 'S01E01-03' }, - { key: 5, value: translate('PrefixedRange'), hint: 'S01E01-E03' } - ]; - - const colonReplacementOptions = [ - { key: 0, value: translate('Delete') }, - { key: 1, value: translate('ReplaceWithDash') }, - { key: 2, value: translate('ReplaceWithSpaceDash') }, - { key: 3, value: translate('ReplaceWithSpaceDashSpace') }, - { key: 4, value: translate('SmartReplace'), hint: translate('SmartReplaceHint') }, - { key: 5, value: translate('Custom'), hint: translate('CustomColonReplacementFormatHint') } - ]; - - const standardEpisodeFormatHelpTexts = []; - const standardEpisodeFormatErrors = []; - const dailyEpisodeFormatHelpTexts = []; - const dailyEpisodeFormatErrors = []; - const animeEpisodeFormatHelpTexts = []; - const animeEpisodeFormatErrors = []; - const seriesFolderFormatHelpTexts = []; - const seriesFolderFormatErrors = []; - const seasonFolderFormatHelpTexts = []; - const seasonFolderFormatErrors = []; - const specialsFolderFormatHelpTexts = []; - const specialsFolderFormatErrors = []; - - if (examplesPopulated) { - if (examples.singleEpisodeExample) { - standardEpisodeFormatHelpTexts.push(`${translate('SingleEpisode')}: ${examples.singleEpisodeExample}`); - } else { - standardEpisodeFormatErrors.push({ message: translate('SingleEpisodeInvalidFormat') }); - } - - if (examples.multiEpisodeExample) { - standardEpisodeFormatHelpTexts.push(`${translate('MultiEpisode')}: ${examples.multiEpisodeExample}`); - } else { - standardEpisodeFormatErrors.push({ message: translate('MultiEpisodeInvalidFormat') }); - } - - if (examples.dailyEpisodeExample) { - dailyEpisodeFormatHelpTexts.push(`${translate('Example')}: ${examples.dailyEpisodeExample}`); - } else { - dailyEpisodeFormatErrors.push({ message: translate('InvalidFormat') }); - } - - if (examples.animeEpisodeExample) { - animeEpisodeFormatHelpTexts.push(`${translate('SingleEpisode')}: ${examples.animeEpisodeExample}`); - } else { - animeEpisodeFormatErrors.push({ message: translate('SingleEpisodeInvalidFormat') }); - } - - if (examples.animeMultiEpisodeExample) { - animeEpisodeFormatHelpTexts.push(`${translate('MultiEpisode')}: ${examples.animeMultiEpisodeExample}`); - } else { - animeEpisodeFormatErrors.push({ message: translate('MultiEpisodeInvalidFormat') }); - } - - if (examples.seriesFolderExample) { - seriesFolderFormatHelpTexts.push(`${translate('Example')}: ${examples.seriesFolderExample}`); - } else { - seriesFolderFormatErrors.push({ message: translate('InvalidFormat') }); - } - - if (examples.seasonFolderExample) { - seasonFolderFormatHelpTexts.push(`${translate('Example')}: ${examples.seasonFolderExample}`); - } else { - seasonFolderFormatErrors.push({ message: translate('InvalidFormat') }); - } - - if (examples.specialsFolderExample) { - specialsFolderFormatHelpTexts.push(`${translate('Example')}: ${examples.specialsFolderExample}`); - } else { - specialsFolderFormatErrors.push({ message: translate('InvalidFormat') }); - } - } - - return ( -
- { - isFetching && - - } - - { - !isFetching && error && - - {translate('NamingSettingsLoadError')} - - } - - { - hasSettings && !isFetching && !error && -
- - {translate('RenameEpisodes')} - - - - - - {translate('ReplaceIllegalCharacters')} - - - - - { - replaceIllegalCharacters ? - - {translate('ColonReplacement')} - - - : - null - } - - { - replaceIllegalCharacters && settings.colonReplacementFormat.value === 5 ? - - {translate('CustomColonReplacement')} - - - : - null - } - - { - renameEpisodes && -
- - {translate('StandardEpisodeFormat')} - - ?} - onChange={onInputChange} - {...settings.standardEpisodeFormat} - helpTexts={standardEpisodeFormatHelpTexts} - errors={[...standardEpisodeFormatErrors, ...settings.standardEpisodeFormat.errors]} - /> - - - - {translate('DailyEpisodeFormat')} - - ?} - onChange={onInputChange} - {...settings.dailyEpisodeFormat} - helpTexts={dailyEpisodeFormatHelpTexts} - errors={[...dailyEpisodeFormatErrors, ...settings.dailyEpisodeFormat.errors]} - /> - - - - {translate('AnimeEpisodeFormat')} - - ?} - onChange={onInputChange} - {...settings.animeEpisodeFormat} - helpTexts={animeEpisodeFormatHelpTexts} - errors={[...animeEpisodeFormatErrors, ...settings.animeEpisodeFormat.errors]} - /> - -
- } - - - {translate('SeriesFolderFormat')} - - ?} - onChange={onInputChange} - {...settings.seriesFolderFormat} - helpTexts={[translate('SeriesFolderFormatHelpText'), ...seriesFolderFormatHelpTexts]} - errors={[...seriesFolderFormatErrors, ...settings.seriesFolderFormat.errors]} - /> - - - - {translate('SeasonFolderFormat')} - - ?} - onChange={onInputChange} - {...settings.seasonFolderFormat} - helpTexts={seasonFolderFormatHelpTexts} - errors={[...seasonFolderFormatErrors, ...settings.seasonFolderFormat.errors]} - /> - - - - {translate('SpecialsFolderFormat')} - - ?} - onChange={onInputChange} - {...settings.specialsFolderFormat} - helpTexts={specialsFolderFormatHelpTexts} - errors={[...specialsFolderFormatErrors, ...settings.specialsFolderFormat.errors]} - /> - - - - {translate('MultiEpisodeStyle')} - - - - - { - namingModalOptions && - - } - - } -
- ); - } - -} - -Naming.propTypes = { - advancedSettings: PropTypes.bool.isRequired, - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - settings: PropTypes.object.isRequired, - hasSettings: PropTypes.bool.isRequired, - examples: PropTypes.object.isRequired, - examplesPopulated: PropTypes.bool.isRequired, - onInputChange: PropTypes.func.isRequired -}; - -export default Naming; diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.tsx b/frontend/src/Settings/MediaManagement/Naming/Naming.tsx new file mode 100644 index 000000000..08dc86910 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.tsx @@ -0,0 +1,525 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import Alert from 'Components/Alert'; +import FieldSet from 'Components/FieldSet'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputButton from 'Components/Form/FormInputButton'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { + fetchNamingExamples, + fetchNamingSettings, + setNamingSettingsValue, +} from 'Store/Actions/settingsActions'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import NamingConfig from 'typings/Settings/NamingConfig'; +import translate from 'Utilities/String/translate'; +import NamingModal from './NamingModal'; +import styles from './Naming.css'; + +const SECTION = 'naming'; + +function createNamingSelector() { + return createSelector( + (state: AppState) => state.settings.advancedSettings, + (state: AppState) => state.settings.namingExamples, + createSettingsSectionSelector(SECTION), + (advancedSettings, namingExamples, sectionSettings) => { + return { + advancedSettings, + examples: namingExamples.item, + examplesPopulated: namingExamples.isPopulated, + ...sectionSettings, + }; + } + ); +} + +interface NamingModalOptions { + name: keyof Pick< + NamingConfig, + | 'standardEpisodeFormat' + | 'dailyEpisodeFormat' + | 'animeEpisodeFormat' + | 'seriesFolderFormat' + | 'seasonFolderFormat' + | 'specialsFolderFormat' + >; + season?: boolean; + episode?: boolean; + daily?: boolean; + anime?: boolean; + additional?: boolean; +} + +function Naming() { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + examples, + examplesPopulated, + } = useSelector(createNamingSelector()); + + const dispatch = useDispatch(); + + const [isNamingModalOpen, setNamingModalOpen, setNamingModalClosed] = + useModalOpenState(false); + const [namingModalOptions, setNamingModalOptions] = + useState(null); + const namingExampleTimeout = useRef>(); + + useEffect(() => { + dispatch(fetchNamingSettings()); + dispatch(fetchNamingExamples()); + + return () => { + dispatch(clearPendingChanges({ section: SECTION })); + }; + }, [dispatch]); + + const handleInputChange = useCallback( + ({ name, value }: { name: string; value: string }) => { + // @ts-expect-error 'setNamingSettingsValue' isn't typed yet + dispatch(setNamingSettingsValue({ name, value })); + + if (namingExampleTimeout.current) { + clearTimeout(namingExampleTimeout.current); + } + + namingExampleTimeout.current = setTimeout(() => { + dispatch(fetchNamingExamples()); + }, 1000); + }, + [dispatch] + ); + + const onStandardNamingModalOpenClick = useCallback(() => { + setNamingModalOpen(); + + setNamingModalOptions({ + name: 'standardEpisodeFormat', + season: true, + episode: true, + additional: true, + }); + }, [setNamingModalOpen, setNamingModalOptions]); + + const onDailyNamingModalOpenClick = useCallback(() => { + setNamingModalOpen(); + + setNamingModalOptions({ + name: 'dailyEpisodeFormat', + season: true, + episode: true, + daily: true, + additional: true, + }); + }, [setNamingModalOpen, setNamingModalOptions]); + + const onAnimeNamingModalOpenClick = useCallback(() => { + setNamingModalOpen(); + + setNamingModalOptions({ + name: 'animeEpisodeFormat', + season: true, + episode: true, + anime: true, + additional: true, + }); + }, [setNamingModalOpen, setNamingModalOptions]); + + const onSeriesFolderNamingModalOpenClick = useCallback(() => { + setNamingModalOpen(); + + setNamingModalOptions({ + name: 'seriesFolderFormat', + }); + }, [setNamingModalOpen, setNamingModalOptions]); + + const onSeasonFolderNamingModalOpenClick = useCallback(() => { + setNamingModalOpen(); + + setNamingModalOptions({ + name: 'seasonFolderFormat', + season: true, + }); + }, [setNamingModalOpen, setNamingModalOptions]); + + const onSpecialsFolderNamingModalOpenClick = useCallback(() => { + setNamingModalOpen(); + + setNamingModalOptions({ + name: 'specialsFolderFormat', + season: true, + }); + }, [setNamingModalOpen, setNamingModalOptions]); + + const renameEpisodes = hasSettings && settings.renameEpisodes.value; + const replaceIllegalCharacters = + hasSettings && settings.replaceIllegalCharacters.value; + + const multiEpisodeStyleOptions = [ + { key: 0, value: translate('Extend'), hint: 'S01E01-02-03' }, + { key: 1, value: translate('Duplicate'), hint: 'S01E01.S01E02' }, + { key: 2, value: translate('Repeat'), hint: 'S01E01E02E03' }, + { key: 3, value: translate('Scene'), hint: 'S01E01-E02-E03' }, + { key: 4, value: translate('Range'), hint: 'S01E01-03' }, + { key: 5, value: translate('PrefixedRange'), hint: 'S01E01-E03' }, + ]; + + const colonReplacementOptions = [ + { key: 0, value: translate('Delete') }, + { key: 1, value: translate('ReplaceWithDash') }, + { key: 2, value: translate('ReplaceWithSpaceDash') }, + { key: 3, value: translate('ReplaceWithSpaceDashSpace') }, + { + key: 4, + value: translate('SmartReplace'), + hint: translate('SmartReplaceHint'), + }, + { + key: 5, + value: translate('Custom'), + hint: translate('CustomColonReplacementFormatHint'), + }, + ]; + + const standardEpisodeFormatHelpTexts = []; + const standardEpisodeFormatErrors = []; + const dailyEpisodeFormatHelpTexts = []; + const dailyEpisodeFormatErrors = []; + const animeEpisodeFormatHelpTexts = []; + const animeEpisodeFormatErrors = []; + const seriesFolderFormatHelpTexts = []; + const seriesFolderFormatErrors = []; + const seasonFolderFormatHelpTexts = []; + const seasonFolderFormatErrors = []; + const specialsFolderFormatHelpTexts = []; + const specialsFolderFormatErrors = []; + + if (examplesPopulated) { + if (examples.singleEpisodeExample) { + standardEpisodeFormatHelpTexts.push( + `${translate('SingleEpisode')}: ${examples.singleEpisodeExample}` + ); + } else { + standardEpisodeFormatErrors.push({ + message: translate('SingleEpisodeInvalidFormat'), + }); + } + + if (examples.multiEpisodeExample) { + standardEpisodeFormatHelpTexts.push( + `${translate('MultiEpisode')}: ${examples.multiEpisodeExample}` + ); + } else { + standardEpisodeFormatErrors.push({ + message: translate('MultiEpisodeInvalidFormat'), + }); + } + + if (examples.dailyEpisodeExample) { + dailyEpisodeFormatHelpTexts.push( + `${translate('Example')}: ${examples.dailyEpisodeExample}` + ); + } else { + dailyEpisodeFormatErrors.push({ message: translate('InvalidFormat') }); + } + + if (examples.animeEpisodeExample) { + animeEpisodeFormatHelpTexts.push( + `${translate('SingleEpisode')}: ${examples.animeEpisodeExample}` + ); + } else { + animeEpisodeFormatErrors.push({ + message: translate('SingleEpisodeInvalidFormat'), + }); + } + + if (examples.animeMultiEpisodeExample) { + animeEpisodeFormatHelpTexts.push( + `${translate('MultiEpisode')}: ${examples.animeMultiEpisodeExample}` + ); + } else { + animeEpisodeFormatErrors.push({ + message: translate('MultiEpisodeInvalidFormat'), + }); + } + + if (examples.seriesFolderExample) { + seriesFolderFormatHelpTexts.push( + `${translate('Example')}: ${examples.seriesFolderExample}` + ); + } else { + seriesFolderFormatErrors.push({ message: translate('InvalidFormat') }); + } + + if (examples.seasonFolderExample) { + seasonFolderFormatHelpTexts.push( + `${translate('Example')}: ${examples.seasonFolderExample}` + ); + } else { + seasonFolderFormatErrors.push({ message: translate('InvalidFormat') }); + } + + if (examples.specialsFolderExample) { + specialsFolderFormatHelpTexts.push( + `${translate('Example')}: ${examples.specialsFolderExample}` + ); + } else { + specialsFolderFormatErrors.push({ message: translate('InvalidFormat') }); + } + } + + return ( +
+ {isFetching ? : null} + + {!isFetching && error ? ( + + {translate('NamingSettingsLoadError')} + + ) : null} + + {hasSettings && !isFetching && !error ? ( +
+ + {translate('RenameEpisodes')} + + + + + + {translate('ReplaceIllegalCharacters')} + + + + + {replaceIllegalCharacters ? ( + + {translate('ColonReplacement')} + + + + ) : null} + + {replaceIllegalCharacters && + settings.colonReplacementFormat.value === 5 ? ( + + {translate('CustomColonReplacement')} + + + + ) : null} + + {renameEpisodes ? ( + <> + + {translate('StandardEpisodeFormat')} + + + ? + + } + onChange={handleInputChange} + {...settings.standardEpisodeFormat} + helpTexts={standardEpisodeFormatHelpTexts} + errors={[ + ...standardEpisodeFormatErrors, + ...settings.standardEpisodeFormat.errors, + ]} + /> + + + + {translate('DailyEpisodeFormat')} + + + ? + + } + onChange={handleInputChange} + {...settings.dailyEpisodeFormat} + helpTexts={dailyEpisodeFormatHelpTexts} + errors={[ + ...dailyEpisodeFormatErrors, + ...settings.dailyEpisodeFormat.errors, + ]} + /> + + + + {translate('AnimeEpisodeFormat')} + + + ? + + } + onChange={handleInputChange} + {...settings.animeEpisodeFormat} + helpTexts={animeEpisodeFormatHelpTexts} + errors={[ + ...animeEpisodeFormatErrors, + ...settings.animeEpisodeFormat.errors, + ]} + /> + + + ) : null} + + + {translate('SeriesFolderFormat')} + + + ? + + } + onChange={handleInputChange} + {...settings.seriesFolderFormat} + helpTexts={[ + translate('SeriesFolderFormatHelpText'), + ...seriesFolderFormatHelpTexts, + ]} + errors={[ + ...seriesFolderFormatErrors, + ...settings.seriesFolderFormat.errors, + ]} + /> + + + + {translate('SeasonFolderFormat')} + + + ? + + } + onChange={handleInputChange} + {...settings.seasonFolderFormat} + helpTexts={seasonFolderFormatHelpTexts} + errors={[ + ...seasonFolderFormatErrors, + ...settings.seasonFolderFormat.errors, + ]} + /> + + + + {translate('SpecialsFolderFormat')} + + + ? + + } + onChange={handleInputChange} + {...settings.specialsFolderFormat} + helpTexts={specialsFolderFormatHelpTexts} + errors={[ + ...specialsFolderFormatErrors, + ...settings.specialsFolderFormat.errors, + ]} + /> + + + + {translate('MultiEpisodeStyle')} + + + + + {namingModalOptions ? ( + + ) : null} + + ) : null} +
+ ); +} + +export default Naming; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js deleted file mode 100644 index 55c3bc597..000000000 --- a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js +++ /dev/null @@ -1,96 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { createSelector } from 'reselect'; -import { clearPendingChanges } from 'Store/Actions/baseActions'; -import { fetchNamingExamples, fetchNamingSettings, setNamingSettingsValue } from 'Store/Actions/settingsActions'; -import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; -import Naming from './Naming'; - -const SECTION = 'naming'; - -function createMapStateToProps() { - return createSelector( - (state) => state.settings.advancedSettings, - (state) => state.settings.namingExamples, - createSettingsSectionSelector(SECTION), - (advancedSettings, namingExamples, sectionSettings) => { - return { - advancedSettings, - examples: namingExamples.item, - examplesPopulated: namingExamples.isPopulated, - ...sectionSettings - }; - } - ); -} - -const mapDispatchToProps = { - fetchNamingSettings, - setNamingSettingsValue, - fetchNamingExamples, - clearPendingChanges -}; - -class NamingConnector extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._namingExampleTimeout = null; - } - - componentDidMount() { - this.props.fetchNamingSettings(); - this.props.fetchNamingExamples(); - } - - componentWillUnmount() { - this.props.clearPendingChanges({ section: `settings.${SECTION}` }); - } - - // - // Control - - _fetchNamingExamples = () => { - this.props.fetchNamingExamples(); - }; - - // - // Listeners - - onInputChange = ({ name, value }) => { - this.props.setNamingSettingsValue({ name, value }); - - if (this._namingExampleTimeout) { - clearTimeout(this._namingExampleTimeout); - } - - this._namingExampleTimeout = setTimeout(this._fetchNamingExamples, 1000); - }; - - // - // Render - - render() { - return ( - - ); - } -} - -NamingConnector.propTypes = { - fetchNamingSettings: PropTypes.func.isRequired, - setNamingSettingsValue: PropTypes.func.isRequired, - fetchNamingExamples: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(NamingConnector); diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js deleted file mode 100644 index 3d1099822..000000000 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ /dev/null @@ -1,646 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import FieldSet from 'Components/FieldSet'; -import SelectInput from 'Components/Form/SelectInput'; -import TextInput from 'Components/Form/TextInput'; -import Icon from 'Components/Icon'; -import Button from 'Components/Link/Button'; -import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; -import Modal from 'Components/Modal/Modal'; -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 { icons, sizes } from 'Helpers/Props'; -import translate from 'Utilities/String/translate'; -import NamingOption from './NamingOption'; -import styles from './NamingModal.css'; - -const separatorOptions = [ - { - key: ' ', - get value() { - return `${translate('Space')} ( )`; - } - }, - { - key: '.', - get value() { - return `${translate('Period')} (.)`; - } - }, - { - key: '_', - get value() { - return `${translate('Underscore')} (_)`; - } - }, - { - key: '-', - get value() { - return `${translate('Dash')} (-)`; - } - } -]; - -const caseOptions = [ - { - key: 'title', - get value() { - return translate('DefaultCase'); - } - }, - { - key: 'lower', - get value() { - return translate('Lowercase'); - } - }, - { - key: 'upper', - get value() { - return translate('Uppercase'); - } - } -]; - -const fileNameTokens = [ - { - token: '{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}', - example: 'The Series Title\'s! (2010) - S01E01 - Episode Title HDTV-720p Proper' - }, - { - token: '{Series Title} - {season:0}x{episode:00} - {Episode Title} {Quality Full}', - example: 'The Series Title\'s! (2010) - 1x01 - Episode Title HDTV-720p Proper' - }, - { - token: '{Series.Title}.S{season:00}E{episode:00}.{EpisodeClean.Title}.{Quality.Full}', - example: 'The.Series.Title\'s!.(2010).S01E01.Episode.Title.HDTV-720p' - } -]; - -const seriesTokens = [ - { token: '{Series Title}', example: 'The Series Title\'s!', footNote: 1 }, - { token: '{Series CleanTitle}', example: 'The Series Title\'s!', footNote: 1 }, - { token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)', footNote: 1 }, - { token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010', footNote: 1 }, - { token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 }, - { token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 }, - { token: '{Series TitleThe}', example: 'Series Title\'s!, The', footNote: 1 }, - { token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The', footNote: 1 }, - { token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)', footNote: 1 }, - { token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010', footNote: 1 }, - { token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 }, - { token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 }, - { token: '{Series TitleFirstCharacter}', example: 'S', footNote: 1 }, - { token: '{Series Year}', example: '2010' } -]; - -const seriesIdTokens = [ - { token: '{ImdbId}', example: 'tt12345' }, - { token: '{TvdbId}', example: '12345' }, - { token: '{TmdbId}', example: '11223' }, - { token: '{TvMazeId}', example: '54321' } -]; - -const seasonTokens = [ - { token: '{season:0}', example: '1' }, - { token: '{season:00}', example: '01' } -]; - -const episodeTokens = [ - { token: '{episode:0}', example: '1' }, - { token: '{episode:00}', example: '01' } -]; - -const airDateTokens = [ - { token: '{Air-Date}', example: '2016-03-20' }, - { token: '{Air Date}', example: '2016 03 20' } -]; - -const absoluteTokens = [ - { token: '{absolute:0}', example: '1' }, - { token: '{absolute:00}', example: '01' }, - { token: '{absolute:000}', example: '001' } -]; - -const episodeTitleTokens = [ - { token: '{Episode Title}', example: 'Episode\'s Title', footNote: 1 }, - { token: '{Episode CleanTitle}', example: 'Episodes Title', footNote: 1 } -]; - -const qualityTokens = [ - { token: '{Quality Full}', example: 'WEBDL-1080p Proper' }, - { token: '{Quality Title}', example: 'WEBDL-1080p' } -]; - -const mediaInfoTokens = [ - { token: '{MediaInfo Simple}', example: 'x264 DTS' }, - { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: 1 }, - - { token: '{MediaInfo AudioCodec}', example: 'DTS' }, - { token: '{MediaInfo AudioChannels}', example: '5.1' }, - { token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: 1 }, - { token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: 1 }, - - { token: '{MediaInfo VideoCodec}', example: 'x264' }, - { token: '{MediaInfo VideoBitDepth}', example: '10' }, - { token: '{MediaInfo VideoDynamicRange}', example: 'HDR' }, - { token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' } -]; - -const otherTokens = [ - { token: '{Release Group}', example: 'Rls Grp', footNote: 1 }, - { token: '{Custom Formats}', example: 'iNTERNAL' }, - { token: '{Custom Format:FormatName}', example: 'AMZN' } -]; - -const otherAnimeTokens = [ - { token: '{Release Hash}', example: 'ABCDEFGH' } -]; - -const originalTokens = [ - { token: '{Original Title}', example: 'The.Series.Title\'s!.S01E01.WEBDL.1080p.x264-EVOLVE' }, - { token: '{Original Filename}', example: 'the.series.title\'s!.s01e01.webdl.1080p.x264-EVOLVE' } -]; - -class NamingModal extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this._selectionStart = null; - this._selectionEnd = null; - - this.state = { - separator: ' ', - case: 'title' - }; - } - - // - // Listeners - - onTokenSeparatorChange = (event) => { - this.setState({ separator: event.value }); - }; - - onTokenCaseChange = (event) => { - this.setState({ case: event.value }); - }; - - onInputSelectionChange = (selectionStart, selectionEnd) => { - this._selectionStart = selectionStart; - this._selectionEnd = selectionEnd; - }; - - onOptionPress = ({ isFullFilename, tokenValue }) => { - const { - name, - value, - onInputChange - } = this.props; - - const selectionStart = this._selectionStart; - const selectionEnd = this._selectionEnd; - - if (isFullFilename) { - onInputChange({ name, value: tokenValue }); - } else if (selectionStart == null) { - onInputChange({ - name, - value: `${value}${tokenValue}` - }); - } else { - const start = value.substring(0, selectionStart); - const end = value.substring(selectionEnd); - const newValue = `${start}${tokenValue}${end}`; - - onInputChange({ name, value: newValue }); - this._selectionStart = newValue.length - 1; - this._selectionEnd = newValue.length - 1; - } - }; - - // - // Render - - render() { - const { - name, - value, - isOpen, - advancedSettings, - season, - episode, - anime, - additional, - onInputChange, - onModalClose - } = this.props; - - const { - separator: tokenSeparator, - case: tokenCase - } = this.state; - - return ( - - - - {translate('FileNameTokens')} - - - -
- - - -
- - { - !advancedSettings && -
-
- { - fileNameTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- } - -
-
- { - seriesTokens.map(({ token, example, footNote }) => { - return ( - - ); - } - ) - } -
- -
- - -
-
- -
-
- { - seriesIdTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- - { - season && -
-
- { - seasonTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- } - - { - episode && -
-
-
- { - episodeTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- -
-
- { - airDateTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- - { - anime && -
-
- { - absoluteTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- } -
- } - - { - additional && -
-
-
- { - episodeTitleTokens.map(({ token, example, footNote }) => { - return ( - - ); - } - ) - } -
-
- - -
-
- -
-
- { - qualityTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
- -
-
- { - mediaInfoTokens.map(({ token, example, footNote }) => { - return ( - - ); - } - ) - } -
- -
- - -
-
- -
-
- { - otherTokens.map(({ token, example, footNote }) => { - return ( - - ); - } - ) - } - - { - anime && otherAnimeTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
- -
- - -
-
- -
-
- { - originalTokens.map(({ token, example }) => { - return ( - - ); - } - ) - } -
-
-
- } -
- - - - - -
-
- ); - } -} - -NamingModal.propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - isOpen: PropTypes.bool.isRequired, - advancedSettings: PropTypes.bool.isRequired, - season: PropTypes.bool.isRequired, - episode: PropTypes.bool.isRequired, - daily: PropTypes.bool.isRequired, - anime: PropTypes.bool.isRequired, - additional: PropTypes.bool.isRequired, - onInputChange: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -NamingModal.defaultProps = { - season: false, - episode: false, - daily: false, - anime: false, - additional: false -}; - -export default NamingModal; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx new file mode 100644 index 000000000..aec03a87c --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.tsx @@ -0,0 +1,591 @@ +import React, { useCallback, useState } from 'react'; +import FieldSet from 'Components/FieldSet'; +import SelectInput from 'Components/Form/SelectInput'; +import TextInput from 'Components/Form/TextInput'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; +import Modal from 'Components/Modal/Modal'; +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 { icons, sizes } from 'Helpers/Props'; +import NamingConfig from 'typings/Settings/NamingConfig'; +import translate from 'Utilities/String/translate'; +import NamingOption from './NamingOption'; +import TokenCase from './TokenCase'; +import TokenSeparator from './TokenSeparator'; +import styles from './NamingModal.css'; + +const separatorOptions: { key: TokenSeparator; value: string }[] = [ + { + key: ' ', + get value() { + return `${translate('Space')} ( )`; + }, + }, + { + key: '.', + get value() { + return `${translate('Period')} (.)`; + }, + }, + { + key: '_', + get value() { + return `${translate('Underscore')} (_)`; + }, + }, + { + key: '-', + get value() { + return `${translate('Dash')} (-)`; + }, + }, +]; + +const caseOptions: { key: TokenCase; value: string }[] = [ + { + key: 'title', + get value() { + return translate('DefaultCase'); + }, + }, + { + key: 'lower', + get value() { + return translate('Lowercase'); + }, + }, + { + key: 'upper', + get value() { + return translate('Uppercase'); + }, + }, +]; + +const fileNameTokens = [ + { + token: + '{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}', + example: + "The Series Title's! (2010) - S01E01 - Episode Title HDTV-720p Proper", + }, + { + token: + '{Series Title} - {season:0}x{episode:00} - {Episode Title} {Quality Full}', + example: + "The Series Title's! (2010) - 1x01 - Episode Title HDTV-720p Proper", + }, + { + token: + '{Series.Title}.S{season:00}E{episode:00}.{EpisodeClean.Title}.{Quality.Full}', + example: "The.Series.Title's!.(2010).S01E01.Episode.Title.HDTV-720p", + }, +]; + +const seriesTokens = [ + { token: '{Series Title}', example: "The Series Title's!", footNote: true }, + { + token: '{Series CleanTitle}', + example: "The Series Title's!", + footNote: true, + }, + { + token: '{Series TitleYear}', + example: "The Series Title's! (2010)", + footNote: true, + }, + { + token: '{Series CleanTitleYear}', + example: "The Series Title's! 2010", + footNote: true, + }, + { + token: '{Series TitleWithoutYear}', + example: "The Series Title's!", + footNote: true, + }, + { + token: '{Series CleanTitleWithoutYear}', + example: "The Series Title's!", + footNote: true, + }, + { + token: '{Series TitleThe}', + example: "Series Title's!, The", + footNote: true, + }, + { + token: '{Series CleanTitleThe}', + example: "Series Title's!, The", + footNote: true, + }, + { + token: '{Series TitleTheYear}', + example: "Series Title's!, The (2010)", + footNote: true, + }, + { + token: '{Series CleanTitleTheYear}', + example: "Series Title's!, The 2010", + footNote: true, + }, + { + token: '{Series TitleTheWithoutYear}', + example: "Series Title's!, The", + footNote: true, + }, + { + token: '{Series CleanTitleTheWithoutYear}', + example: "Series Title's!, The", + footNote: true, + }, + { token: '{Series TitleFirstCharacter}', example: 'S', footNote: true }, + { token: '{Series Year}', example: '2010' }, +]; + +const seriesIdTokens = [ + { token: '{ImdbId}', example: 'tt12345' }, + { token: '{TvdbId}', example: '12345' }, + { token: '{TmdbId}', example: '11223' }, + { token: '{TvMazeId}', example: '54321' }, +]; + +const seasonTokens = [ + { token: '{season:0}', example: '1' }, + { token: '{season:00}', example: '01' }, +]; + +const episodeTokens = [ + { token: '{episode:0}', example: '1' }, + { token: '{episode:00}', example: '01' }, +]; + +const airDateTokens = [ + { token: '{Air-Date}', example: '2016-03-20' }, + { token: '{Air Date}', example: '2016 03 20' }, +]; + +const absoluteTokens = [ + { token: '{absolute:0}', example: '1' }, + { token: '{absolute:00}', example: '01' }, + { token: '{absolute:000}', example: '001' }, +]; + +const episodeTitleTokens = [ + { token: '{Episode Title}', example: "Episode's Title", footNote: true }, + { token: '{Episode CleanTitle}', example: 'Episodes Title', footNote: true }, +]; + +const qualityTokens = [ + { token: '{Quality Full}', example: 'WEBDL-1080p Proper' }, + { token: '{Quality Title}', example: 'WEBDL-1080p' }, +]; + +const mediaInfoTokens = [ + { token: '{MediaInfo Simple}', example: 'x264 DTS' }, + { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: true }, + + { token: '{MediaInfo AudioCodec}', example: 'DTS' }, + { token: '{MediaInfo AudioChannels}', example: '5.1' }, + { token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: true }, + { token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: true }, + + { token: '{MediaInfo VideoCodec}', example: 'x264' }, + { token: '{MediaInfo VideoBitDepth}', example: '10' }, + { token: '{MediaInfo VideoDynamicRange}', example: 'HDR' }, + { token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' }, +]; + +const otherTokens = [ + { token: '{Release Group}', example: 'Rls Grp', footNote: true }, + { token: '{Custom Formats}', example: 'iNTERNAL' }, + { token: '{Custom Format:FormatName}', example: 'AMZN' }, +]; + +const otherAnimeTokens = [{ token: '{Release Hash}', example: 'ABCDEFGH' }]; + +const originalTokens = [ + { + token: '{Original Title}', + example: "The.Series.Title's!.S01E01.WEBDL.1080p.x264-EVOLVE", + }, + { + token: '{Original Filename}', + example: "the.series.title's!.s01e01.webdl.1080p.x264-EVOLVE", + }, +]; + +interface NamingModalProps { + isOpen: boolean; + name: keyof Pick< + NamingConfig, + | 'standardEpisodeFormat' + | 'dailyEpisodeFormat' + | 'animeEpisodeFormat' + | 'seriesFolderFormat' + | 'seasonFolderFormat' + | 'specialsFolderFormat' + >; + value: string; + advancedSettings: boolean; + season?: boolean; + episode?: boolean; + daily?: boolean; + anime?: boolean; + additional?: boolean; + onInputChange: ({ name, value }: { name: string; value: string }) => void; + onModalClose: () => void; +} + +function NamingModal(props: NamingModalProps) { + const { + isOpen, + name, + value, + advancedSettings, + season = false, + episode = false, + anime = false, + additional = false, + onInputChange, + onModalClose, + } = props; + + const [tokenSeparator, setTokenSeparator] = useState(' '); + const [tokenCase, setTokenCase] = useState('title'); + const [selectionStart, setSelectionStart] = useState(null); + const [selectionEnd, setSelectionEnd] = useState(null); + + const handleTokenSeparatorChange = useCallback( + ({ value }: { value: TokenSeparator }) => { + setTokenSeparator(value); + }, + [setTokenSeparator] + ); + + const handleTokenCaseChange = useCallback( + ({ value }: { value: TokenCase }) => { + setTokenCase(value); + }, + [setTokenCase] + ); + + const handleInputSelectionChange = useCallback( + (selectionStart: number, selectionEnd: number) => { + setSelectionStart(selectionStart); + setSelectionEnd(selectionEnd); + }, + [setSelectionStart, setSelectionEnd] + ); + + const handleOptionPress = useCallback( + ({ + isFullFilename, + tokenValue, + }: { + isFullFilename: boolean; + tokenValue: string; + }) => { + if (isFullFilename) { + onInputChange({ name, value: tokenValue }); + } else if (selectionStart == null || selectionEnd == null) { + onInputChange({ + name, + value: `${value}${tokenValue}`, + }); + } else { + const start = value.substring(0, selectionStart); + const end = value.substring(selectionEnd); + const newValue = `${start}${tokenValue}${end}`; + + onInputChange({ name, value: newValue }); + + setSelectionStart(newValue.length - 1); + setSelectionEnd(newValue.length - 1); + } + }, + [name, value, selectionEnd, selectionStart, onInputChange] + ); + + return ( + + + + {episode + ? translate('FileNameTokens') + : translate('FolderNameTokens')} + + + +
+ + + +
+ + {advancedSettings ? null : ( +
+
+ {fileNameTokens.map(({ token, example }) => ( + + ))} +
+
+ )} + +
+
+ {seriesTokens.map(({ token, example, footNote }) => ( + + ))} +
+ +
+ + +
+
+ +
+
+ {seriesIdTokens.map(({ token, example }) => ( + + ))} +
+
+ + {season ? ( +
+
+ {seasonTokens.map(({ token, example }) => ( + + ))} +
+
+ ) : null} + + {episode ? ( +
+
+
+ {episodeTokens.map(({ token, example }) => ( + + ))} +
+
+ +
+
+ {airDateTokens.map(({ token, example }) => ( + + ))} +
+
+ + {anime ? ( +
+
+ {absoluteTokens.map(({ token, example }) => ( + + ))} +
+
+ ) : null} +
+ ) : null} + + {additional ? ( +
+
+
+ {episodeTitleTokens.map(({ token, example, footNote }) => ( + + ))} +
+
+ + +
+
+ +
+
+ {qualityTokens.map(({ token, example }) => ( + + ))} +
+
+ +
+
+ {mediaInfoTokens.map(({ token, example, footNote }) => ( + + ))} +
+ +
+ + +
+
+ +
+
+ {otherTokens.map(({ token, example, footNote }) => ( + + ))} + + {anime + ? otherAnimeTokens.map(({ token, example }) => ( + + )) + : null} +
+ +
+ + +
+
+ +
+
+ {originalTokens.map(({ token, example }) => ( + + ))} +
+
+
+ ) : null} +
+ + + + + + +
+
+ ); +} + +export default NamingModal; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css index 204c93d0e..1fb8a05eb 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -45,6 +45,10 @@ } } +.title { + text-transform: none; +} + .lower { text-transform: lowercase; } diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts index a060f6218..5c50bfab2 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css.d.ts @@ -8,6 +8,7 @@ interface CssExports { 'lower': string; 'option': string; 'small': string; + 'title': string; 'token': string; 'upper': string; } diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js deleted file mode 100644 index 6373c11e3..000000000 --- a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js +++ /dev/null @@ -1,93 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import Icon from 'Components/Icon'; -import Link from 'Components/Link/Link'; -import { icons, sizes } from 'Helpers/Props'; -import styles from './NamingOption.css'; - -class NamingOption extends Component { - - // - // Listeners - - onPress = () => { - const { - token, - tokenSeparator, - tokenCase, - isFullFilename, - onPress - } = this.props; - - let tokenValue = token; - - tokenValue = tokenValue.replace(/ /g, tokenSeparator); - - if (tokenCase === 'lower') { - tokenValue = token.toLowerCase(); - } else if (tokenCase === 'upper') { - tokenValue = token.toUpperCase(); - } - - onPress({ isFullFilename, tokenValue }); - }; - - // - // Render - render() { - const { - token, - tokenSeparator, - example, - footNote, - tokenCase, - isFullFilename, - size - } = this.props; - - return ( - -
- {token.replace(/ /g, tokenSeparator)} -
- -
- {example.replace(/ /g, tokenSeparator)} - - { - footNote !== 0 && - - } -
- - ); - } -} - -NamingOption.propTypes = { - token: PropTypes.string.isRequired, - example: PropTypes.string.isRequired, - footNote: PropTypes.number.isRequired, - tokenSeparator: PropTypes.string.isRequired, - tokenCase: PropTypes.string.isRequired, - isFullFilename: PropTypes.bool.isRequired, - size: PropTypes.oneOf([sizes.SMALL, sizes.LARGE]), - onPress: PropTypes.func.isRequired -}; - -NamingOption.defaultProps = { - footNote: 0, - size: sizes.SMALL, - isFullFilename: false -}; - -export default NamingOption; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.tsx b/frontend/src/Settings/MediaManagement/Naming/NamingOption.tsx new file mode 100644 index 000000000..e9bcf11ff --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.tsx @@ -0,0 +1,77 @@ +import classNames from 'classnames'; +import React, { useCallback } from 'react'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import { icons } from 'Helpers/Props'; +import { Size } from 'Helpers/Props/sizes'; +import TokenCase from './TokenCase'; +import TokenSeparator from './TokenSeparator'; +import styles from './NamingOption.css'; + +interface NamingOptionProps { + token: string; + tokenSeparator: TokenSeparator; + example: string; + tokenCase: TokenCase; + isFullFilename?: boolean; + footNote?: boolean; + size?: Extract; + onPress: ({ + isFullFilename, + tokenValue, + }: { + isFullFilename: boolean; + tokenValue: string; + }) => void; +} + +function NamingOption(props: NamingOptionProps) { + const { + token, + tokenSeparator, + example, + tokenCase, + isFullFilename = false, + footNote = false, + size = 'small', + onPress, + } = props; + + const handlePress = useCallback(() => { + let tokenValue = token; + + tokenValue = tokenValue.replace(/ /g, tokenSeparator); + + if (tokenCase === 'lower') { + tokenValue = token.toLowerCase(); + } else if (tokenCase === 'upper') { + tokenValue = token.toUpperCase(); + } + + onPress({ isFullFilename, tokenValue }); + }, [token, tokenCase, tokenSeparator, isFullFilename, onPress]); + + return ( + +
{token.replace(/ /g, tokenSeparator)}
+ +
+ {example.replace(/ /g, tokenSeparator)} + + {footNote ? ( + + ) : null} +
+ + ); +} + +export default NamingOption; diff --git a/frontend/src/Settings/MediaManagement/Naming/TokenCase.ts b/frontend/src/Settings/MediaManagement/Naming/TokenCase.ts new file mode 100644 index 000000000..280ef307d --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/TokenCase.ts @@ -0,0 +1,3 @@ +type TokenCase = 'title' | 'lower' | 'upper'; + +export default TokenCase; diff --git a/frontend/src/Settings/MediaManagement/Naming/TokenSeparator.ts b/frontend/src/Settings/MediaManagement/Naming/TokenSeparator.ts new file mode 100644 index 000000000..5ef86a6a1 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/TokenSeparator.ts @@ -0,0 +1,3 @@ +type TokenSeparator = ' ' | '.' | '_' | '-'; + +export default TokenSeparator; diff --git a/frontend/src/typings/Settings/NamingConfig.ts b/frontend/src/typings/Settings/NamingConfig.ts new file mode 100644 index 000000000..e013b89cc --- /dev/null +++ b/frontend/src/typings/Settings/NamingConfig.ts @@ -0,0 +1,13 @@ +export default interface NamingConfig { + renameEpisodes: boolean; + replaceIllegalCharacters: boolean; + colonReplacementFormat: number; + customColonReplacementFormat: string; + multiEpisodeStyle: number; + standardEpisodeFormat: string; + dailyEpisodeFormat: string; + animeEpisodeFormat: string; + seriesFolderFormat: string; + seasonFolderFormat: string; + specialsFolderFormat: string; +} diff --git a/frontend/src/typings/Settings/NamingExample.ts b/frontend/src/typings/Settings/NamingExample.ts new file mode 100644 index 000000000..52ffc4d27 --- /dev/null +++ b/frontend/src/typings/Settings/NamingExample.ts @@ -0,0 +1,10 @@ +export default interface NamingExample { + singleEpisodeExample: string; + multiEpisodeExample: string; + dailyEpisodeExample: string; + animeEpisodeExample: string; + animeMultiEpisodeExample: string; + seriesFolderExample: string; + seasonFolderExample: string; + specialsFolderExample: string; +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index abf08cbc3..3ade2c46b 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -727,6 +727,7 @@ "FirstDayOfWeek": "First Day of Week", "Fixed": "Fixed", "Folder": "Folder", + "FolderNameTokens": "Folder Name Tokens", "Folders": "Folders", "Forecast": "Forecast", "FormatAgeDay": "day",