diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index 881b2599c..2a6d359be 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -22,7 +22,7 @@ import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementCo import MetadataSettings from 'Settings/Metadata/MetadataSettings'; import NotificationSettings from 'Settings/Notifications/NotificationSettings'; import Profiles from 'Settings/Profiles/Profiles'; -import Quality from 'Settings/Quality/Quality'; +import QualityConnector from 'Settings/Quality/QualityConnector'; import Settings from 'Settings/Settings'; import TagSettings from 'Settings/Tags/TagSettings'; import UISettingsConnector from 'Settings/UI/UISettingsConnector'; @@ -143,7 +143,7 @@ function AppRoutes(props) { { + this.props.dispatchFetchQualityDefinitions(); + }; + handleQueue = () => { if (this.props.isQueuePopulated) { this.props.dispatchFetchQueue(); @@ -335,6 +341,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 8ba72489f..49c9df5d0 100644 --- a/frontend/src/Settings/Quality/Quality.js +++ b/frontend/src/Settings/Quality/Quality.js @@ -1,9 +1,14 @@ -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 PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import { icons } from 'Helpers/Props'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector'; +import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal'; class Quality extends Component { @@ -17,7 +22,8 @@ class Quality extends Component { this.state = { isSaving: false, - hasPendingChanges: false + hasPendingChanges: false, + isConfirmQualityDefinitionResetModalOpen: false }; } @@ -32,6 +38,14 @@ class Quality extends Component { this.setState(payload); }; + onResetQualityDefinitionsPress = () => { + this.setState({ isConfirmQualityDefinitionResetModalOpen: true }); + }; + + onCloseResetQualityDefinitionsModal = () => { + this.setState({ isConfirmQualityDefinitionResetModalOpen: false }); + }; + onSavePress = () => { if (this._saveCallback) { this._saveCallback(); @@ -44,6 +58,7 @@ class Quality extends Component { render() { const { isSaving, + isResettingQualityDefinitions, hasPendingChanges } = this.state; @@ -52,6 +67,18 @@ class Quality extends Component { + + + + + } onSavePress={this.onSavePress} /> @@ -61,9 +88,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..8cc9219cb --- /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 * as commandNames from 'Commands/commandNames'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +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..ee9caa260 --- /dev/null +++ b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModal.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import { sizes } from 'Helpers/Props'; +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..9e2540632 --- /dev/null +++ b/frontend/src/Settings/Quality/Reset/ResetQualityDefinitionsModalContent.js @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Button from 'Components/Link/Button'; +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 { inputTypes, kinds } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +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 ( + + + {translate('ResetQualityDefinitions')} + + + +
+ {translate('AreYouSureYouWantToResetQualityDefinitions')} +
+ + + {translate('ResetTitles')} + + + + +
+ + + + + + +
+ ); + } +} + +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..645cac1e1 --- /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 createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import ResetQualityDefinitionsModalContent from './ResetQualityDefinitionsModalContent'; + +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/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 97297a3fd..76fe5b98f 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -66,6 +66,7 @@ "AreYouSureYouWantToRemoveSelectedItemFromQueue": "Are you sure you want to remove 1 item from the queue?", "AreYouSureYouWantToRemoveSelectedItemsFromQueue": "Are you sure you want to remove {0} items from the queue?", "AreYouSureYouWantToRemoveTheSelectedItemsFromBlocklist": "Are you sure you want to remove the selected items from the blocklist?", + "AreYouSureYouWantToResetQualityDefinitions": "Are you sure you want to reset quality definitions?", "AreYouSureYouWantToResetYourAPIKey": "Are you sure you want to reset your API Key?", "AsAllDayHelpText": "Events will appear as all-day events in your calendar", "AudioInfo": "Audio Info", @@ -846,6 +847,10 @@ "RescanMovieFolderAfterRefresh": "Rescan Movie Folder after Refresh", "Reset": "Reset", "ResetAPIKey": "Reset API Key", + "ResetDefinitions": "Reset Definitions", + "ResetQualityDefinitions": "Reset Quality Definitions", + "ResetTitles": "Reset Titles", + "ResetTitlesHelpText": "Reset definition titles as well as values", "Restart": "Restart", "RestartNow": "Restart Now", "RestartRadarr": "Restart Radarr", 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/QualityDefinitionService.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs index 166532ee5..968a69407 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs @@ -1,10 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; +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; @@ -105,5 +107,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/Radarr.Api.V3/Qualities/QualityDefinitionController.cs b/src/Radarr.Api.V3/Qualities/QualityDefinitionController.cs index 37ca3f0b0..a662f9f4f 100644 --- a/src/Radarr.Api.V3/Qualities/QualityDefinitionController.cs +++ b/src/Radarr.Api.V3/Qualities/QualityDefinitionController.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Qualities; +using NzbDrone.SignalR; using Radarr.Http; using Radarr.Http.REST; using Radarr.Http.REST.Attributes; @@ -9,11 +12,12 @@ using Radarr.Http.REST.Attributes; namespace Radarr.Api.V3.Qualities { [V3ApiController] - public class QualityDefinitionController : RestController + public class QualityDefinitionController : RestControllerWithSignalR, IHandle { private readonly IQualityDefinitionService _qualityDefinitionService; - public QualityDefinitionController(IQualityDefinitionService qualityDefinitionService) + public QualityDefinitionController(IQualityDefinitionService qualityDefinitionService, IBroadcastSignalRMessage signalRBroadcaster) + : base(signalRBroadcaster) { _qualityDefinitionService = qualityDefinitionService; } @@ -40,7 +44,7 @@ namespace Radarr.Api.V3.Qualities [HttpPut("update")] public object UpdateMany([FromBody] List resource) { - //Read from request + // Read from request var qualityDefinitions = resource .ToModel() .ToList(); @@ -50,5 +54,14 @@ namespace Radarr.Api.V3.Qualities return Accepted(_qualityDefinitionService.All() .ToResource()); } + + [NonAction] + public void Handle(CommandExecutedEvent message) + { + if (message.Command.Name == "ResetQualityDefinitions") + { + BroadcastResourceChange(ModelAction.Sync); + } + } } }