diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index 102681071..ad7b7865c 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -19,7 +19,7 @@ import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import Settings from 'Settings/Settings'; import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector'; import Profiles from 'Settings/Profiles/Profiles'; -import Quality from 'Settings/Quality/Quality'; +import QualityConnector from 'Settings/Quality/QualityConnector'; import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector'; import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; @@ -158,7 +158,7 @@ function AppRoutes(props) { { + this.props.dispatchFetchQualityDefinitions(); + } + handleQueue = () => { if (this.props.isQueuePopulated) { this.props.dispatchFetchQueue(); @@ -377,6 +383,7 @@ SignalRConnector.propTypes = { dispatchUpdateItem: PropTypes.func.isRequired, dispatchRemoveItem: PropTypes.func.isRequired, dispatchFetchHealth: PropTypes.func.isRequired, + dispatchFetchQualityDefinitions: PropTypes.func.isRequired, dispatchFetchQueue: PropTypes.func.isRequired, dispatchFetchQueueDetails: PropTypes.func.isRequired, dispatchFetchRootFolders: PropTypes.func.isRequired, diff --git a/frontend/src/Settings/Quality/Quality.js b/frontend/src/Settings/Quality/Quality.js index ce7c34952..06e38b062 100644 --- a/frontend/src/Settings/Quality/Quality.js +++ b/frontend/src/Settings/Quality/Quality.js @@ -1,8 +1,13 @@ -import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import React, { Component, Fragment } from 'react'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import { icons } from 'Helpers/Props'; +import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal'; class Quality extends Component { @@ -16,7 +21,8 @@ class Quality extends Component { this.state = { isSaving: false, - hasPendingChanges: false + hasPendingChanges: false, + isConfirmQualityDefinitionResetModalOpen: false }; } @@ -31,6 +37,14 @@ class Quality extends Component { this.setState(payload); } + onResetQualityDefinitionsPress = () => { + this.setState({ isConfirmQualityDefinitionResetModalOpen: true }); + } + + onCloseResetQualityDefinitionsModal = () => { + this.setState({ isConfirmQualityDefinitionResetModalOpen: false }); + } + onSavePress = () => { if (this._saveCallback) { this._saveCallback(); @@ -43,6 +57,7 @@ class Quality extends Component { render() { const { isSaving, + isResettingQualityDefinitions, hasPendingChanges } = this.state; @@ -51,6 +66,18 @@ class Quality extends Component { + + + + + } onSavePress={this.onSavePress} /> @@ -60,9 +87,18 @@ class Quality extends Component { onChildStateChange={this.onChildStateChange} /> + + ); } } +Quality.propTypes = { + isResettingQualityDefinitions: PropTypes.bool.isRequired +}; + export default Quality; diff --git a/frontend/src/Settings/Quality/QualityConnector.js b/frontend/src/Settings/Quality/QualityConnector.js new file mode 100644 index 000000000..5bd68d6b3 --- /dev/null +++ b/frontend/src/Settings/Quality/QualityConnector.js @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import * as commandNames from 'Commands/commandNames'; +import Quality from './Quality'; + +function createMapStateToProps() { + return createSelector( + createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS), + (isResettingQualityDefinitions) => { + return { + isResettingQualityDefinitions + }; + } + ); +} + +class QualityConnector extends Component { + + // + // Render + + render() { + return ( + + ); + } +} + +QualityConnector.propTypes = { + isResettingQualityDefinitions: PropTypes.bool.isRequired +}; + +export default connect(createMapStateToProps)(QualityConnector); diff --git a/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModal.js b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModal.js new file mode 100644 index 000000000..dc83b5aca --- /dev/null +++ b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModal.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { sizes } from 'Helpers/Props'; +import Modal from 'Components/Modal/Modal'; +import ResetQualityDefinitionsModalContentConnector from './ResetQualityDefinitionsModalContentConnector'; + +function ResetQualityDefinitionsModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +ResetQualityDefinitionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ResetQualityDefinitionsModal; diff --git a/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContent.css b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContent.css new file mode 100644 index 000000000..99c50adbe --- /dev/null +++ b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContent.css @@ -0,0 +1,3 @@ +.messageContainer { + margin-bottom: 20px; +} diff --git a/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContent.js b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContent.js new file mode 100644 index 000000000..d4b9b7dc6 --- /dev/null +++ b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContent.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './ResetQualityDefinitionsModalContent.css'; + +class ResetQualityDefinitionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + resetDefinitionTitles: false + }; + } + + // + // Listeners + + onResetDefinitionTitlesChange = ({ value }) => { + this.setState({ resetDefinitionTitles: value }); + } + + onResetQualityDefinitionsConfirmed = () => { + const resetDefinitionTitles = this.state.resetDefinitionTitles; + + this.setState({ resetDefinitionTitles: false }); + this.props.onResetQualityDefinitions(resetDefinitionTitles); + } + + // + // Render + + render() { + const { + onModalClose, + isResettingQualityDefinitions + } = this.props; + + const resetDefinitionTitles = this.state.resetDefinitionTitles; + + return ( + + + Reset Quality Definitions + + + + + Are you sure you want to reset quality definitions? + + + + Reset Titles + + + + + + + + + Cancel + + + + Reset + + + + ); + } +} + +ResetQualityDefinitionsModalContent.propTypes = { + onResetQualityDefinitions: PropTypes.func.isRequired, + isResettingQualityDefinitions: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ResetQualityDefinitionsModalContent; diff --git a/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContentConnector.js b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContentConnector.js new file mode 100644 index 000000000..aafde4ccf --- /dev/null +++ b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContentConnector.js @@ -0,0 +1,54 @@ +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 ResetQualityDefinitionsModalContent from './ResetQualityDefinitionsModalContent'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; + +function createMapStateToProps() { + return createSelector( + createCommandExecutingSelector(commandNames.RESET_QUALITY_DEFINITIONS), + (isResettingQualityDefinitions) => { + return { + isResettingQualityDefinitions + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand +}; + +class ResetQualityDefinitionsModalContentConnector extends Component { + + // + // Listeners + + onResetQualityDefinitions = (resetTitles) => { + this.props.executeCommand({ name: commandNames.RESET_QUALITY_DEFINITIONS, resetTitles }); + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ResetQualityDefinitionsModalContentConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + isResettingQualityDefinitions: PropTypes.bool.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ResetQualityDefinitionsModalContentConnector); diff --git a/src/NzbDrone.Core/Qualities/Commands/ResetQualityDefinitionsCommand.cs b/src/NzbDrone.Core/Qualities/Commands/ResetQualityDefinitionsCommand.cs new file mode 100644 index 000000000..d588ef822 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/Commands/ResetQualityDefinitionsCommand.cs @@ -0,0 +1,14 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.Qualities.Commands +{ + public class ResetQualityDefinitionsCommand : Command + { + public bool ResetTitles { get; set; } + + public ResetQualityDefinitionsCommand(bool resetTitles = false) + { + ResetTitles = resetTitles; + } + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs index 20286d275..49941710c 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionRepository.cs @@ -14,7 +14,5 @@ namespace NzbDrone.Core.Qualities : base(database, eventAggregator) { } - - } } diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs index 2b53417ff..015170943 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs @@ -5,6 +5,8 @@ using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; using System; using NzbDrone.Common.Cache; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Qualities.Commands; namespace NzbDrone.Core.Qualities { @@ -17,7 +19,7 @@ namespace NzbDrone.Core.Qualities QualityDefinition Get(Quality quality); } - public class QualityDefinitionService : IQualityDefinitionService, IHandle + public class QualityDefinitionService : IQualityDefinitionService, IExecute, IHandle { private readonly IQualityDefinitionRepository _repo; private readonly ICached> _cache; @@ -106,5 +108,28 @@ namespace NzbDrone.Core.Qualities InsertMissingDefinitions(); } + + public void Execute(ResetQualityDefinitionsCommand message) + { + List updateList = new List(); + + var allDefinitions = Quality.DefaultQualityDefinitions.OrderBy(d => d.Weight).ToList(); + var existingDefinitions = _repo.All().ToList(); + + foreach (var definition in allDefinitions) + { + var existing = existingDefinitions.SingleOrDefault(d => d.Quality == definition.Quality); + + existing.MinSize = definition.MinSize; + existing.MaxSize = definition.MaxSize; + existing.Title = message.ResetTitles ? definition.Title : existing.Title; + + updateList.Add(existing); + } + + _repo.UpdateMany(updateList); + + _cache.Clear(); + } } } diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs index 80f705450..f83e226aa 100644 --- a/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs @@ -1,17 +1,21 @@ using System.Collections.Generic; using System.Linq; using Nancy; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Qualities; +using NzbDrone.SignalR; using Sonarr.Http; using Sonarr.Http.Extensions; namespace Sonarr.Api.V3.Qualities { - public class QualityDefinitionModule : SonarrRestModule + public class QualityDefinitionModule : SonarrRestModuleWithSignalR, IHandle { private readonly IQualityDefinitionService _qualityDefinitionService; - public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService) + public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService, IBroadcastSignalRMessage signalRBroadcaster) + : base(signalRBroadcaster) { _qualityDefinitionService = qualityDefinitionService; @@ -50,5 +54,13 @@ namespace Sonarr.Api.V3.Qualities .ToResource() , HttpStatusCode.Accepted); } + + public void Handle(CommandExecutedEvent message) + { + if (message.Command.Name == "ResetQualityDefinitions") + { + BroadcastResourceChange(ModelAction.Sync); + } + } } } \ No newline at end of file