From dbb6ef7664ea033642194d09e799a52aabbea59d Mon Sep 17 00:00:00 2001 From: Qstick Date: Sun, 23 Jan 2022 23:42:41 -0600 Subject: [PATCH] New: Custom Formats Co-Authored-By: ta264 --- frontend/build/webpack.config.js | 3 +- .../src/Activity/Blocklist/BlocklistRow.js | 13 + .../History/Details/HistoryDetails.js | 43 +- frontend/src/Activity/History/HistoryRow.css | 6 + frontend/src/Activity/History/HistoryRow.js | 25 + frontend/src/Activity/Queue/QueueRow.js | 13 + frontend/src/App/AppRoutes.js | 6 + frontend/src/Book/BookFormats.js | 33 ++ .../src/Components/Form/FormInputGroup.js | 4 + frontend/src/Components/Form/TextArea.css | 19 + frontend/src/Components/Form/TextArea.js | 172 +++++++ .../Components/Form/TextTagInputConnector.js | 6 +- .../src/Components/Link/ClipboardButton.js | 16 +- .../Components/Page/Sidebar/PageSidebar.js | 7 +- .../Table/TableOptions/TableOptionsModal.js | 4 +- frontend/src/Helpers/Props/icons.js | 2 + frontend/src/Helpers/Props/inputTypes.js | 2 + .../InteractiveImportModalContent.js | 9 + .../Interactive/InteractiveImportRow.css | 4 + .../Interactive/InteractiveImportRow.js | 24 +- .../InteractiveSearch/InteractiveSearch.js | 5 +- .../InteractiveSearchRow.css | 4 +- .../InteractiveSearch/InteractiveSearchRow.js | 20 +- .../CustomFormatSettingsConnector.js | 32 ++ .../CustomFormats/CustomFormat.css | 49 ++ .../CustomFormats/CustomFormat.js | 174 +++++++ .../CustomFormats/CustomFormats.css | 21 + .../CustomFormats/CustomFormats.js | 115 +++++ .../CustomFormats/CustomFormatsConnector.js | 63 +++ .../CustomFormats/EditCustomFormatModal.js | 61 +++ .../EditCustomFormatModalConnector.js | 43 ++ .../EditCustomFormatModalContent.css | 27 ++ .../EditCustomFormatModalContent.js | 256 +++++++++++ .../EditCustomFormatModalContentConnector.js | 102 +++++ .../CustomFormats/ExportCustomFormatModal.js | 61 +++ .../ExportCustomFormatModalContent.css | 5 + .../ExportCustomFormatModalContent.js | 84 ++++ ...ExportCustomFormatModalContentConnector.js | 83 ++++ .../CustomFormats/ImportCustomFormatModal.js | 61 +++ .../ImportCustomFormatModalContent.css | 5 + .../ImportCustomFormatModalContent.js | 151 ++++++ ...ImportCustomFormatModalContentConnector.js | 145 ++++++ .../Specifications/AddSpecificationItem.css | 44 ++ .../Specifications/AddSpecificationItem.js | 110 +++++ .../Specifications/AddSpecificationModal.js | 25 + .../AddSpecificationModalContent.css | 5 + .../AddSpecificationModalContent.js | 101 +++++ .../AddSpecificationModalContentConnector.js | 70 +++ .../AddSpecificationPresetMenuItem.js | 49 ++ .../Specifications/EditSpecificationModal.js | 27 ++ .../EditSpecificationModalConnector.js | 50 ++ .../EditSpecificationModalContent.css | 5 + .../EditSpecificationModalContent.js | 160 +++++++ .../EditSpecificationModalContentConnector.js | 78 ++++ .../Specifications/Specification.css | 38 ++ .../Specifications/Specification.js | 139 ++++++ .../MediaManagement/MediaManagement.js | 2 +- .../MediaManagement/Naming/NamingModal.js | 2 +- .../Delay/EditDelayProfileModalContent.js | 53 ++- .../EditQualityProfileModalContent.css | 17 +- .../Quality/EditQualityProfileModalContent.js | 69 ++- ...EditQualityProfileModalContentConnector.js | 48 +- .../Quality/QualityProfileFormatItem.css | 45 ++ .../Quality/QualityProfileFormatItem.js | 68 +++ .../Quality/QualityProfileFormatItems.css | 31 ++ .../Quality/QualityProfileFormatItems.js | 159 +++++++ .../Release/EditReleaseProfileModalContent.js | 40 +- ...EditReleaseProfileModalContentConnector.js | 5 +- .../Profiles/Release/ReleaseProfile.css | 6 + .../Profiles/Release/ReleaseProfile.js | 48 +- frontend/src/Settings/Settings.js | 11 + .../Tags/Details/TagDetailsModalContent.js | 5 +- .../Settings/customFormatSpecifications.js | 193 ++++++++ .../Store/Actions/Settings/customFormats.js | 108 +++++ .../src/Store/Actions/blocklistActions.js | 6 + frontend/src/Store/Actions/historyActions.js | 24 +- frontend/src/Store/Actions/queueActions.js | 6 + frontend/src/Store/Actions/releaseActions.js | 6 + frontend/src/Store/Actions/settingsActions.js | 10 + .../Number/formatPreferredWordScore.js | 16 + frontend/src/Utilities/State/getNextId.js | 5 + .../src/Utilities/State/getProviderState.js | 17 +- package.json | 1 + .../CustomFormats/CustomFormatsTestHelpers.cs | 32 ++ .../026_add_custom_formatsFixture.cs | 428 ++++++++++++++++++ ...matAllowedByProfileSpecificationFixture.cs | 116 +++++ .../CutoffSpecificationFixture.cs | 50 +- .../PrioritizeDownloadDecisionFixture.cs | 26 +- .../QueueSpecificationFixture.cs | 45 +- ...ReleaseRestrictionsSpecificationFixture.cs | 19 +- .../RssSync/DelaySpecificationFixture.cs | 58 ++- .../HistorySpecificationFixture.cs | 18 +- .../UpgradeAllowedSpecificationFixture.cs | 192 ++++++-- .../UpgradeDiskSpecificationFixture.cs | 23 +- .../UpgradeSpecificationFixture.cs | 15 +- .../PendingReleaseServiceTests/AddFixture.cs | 1 + .../RemovePendingFixture.cs | 4 +- ...CleanupQualityProfileFormatItemsFixture.cs | 134 ++++++ .../TrackImport/GetSceneNameFixture.cs | 142 ++++++ .../SameFileSpecificationFixture.cs | 106 ----- .../FileNameBuilderTests/CleanTitleFixture.cs | 5 + .../FileNameBuilderFixture.cs | 5 + .../FileNameBuilderTests/TitleTheFixture.cs | 5 + ...ure.cs => QualityProfileServiceFixture.cs} | 8 +- .../PreferredWordService/CalculateFixture.cs | 94 ---- .../GetMatchingPreferredWordsFixture.cs | 77 ---- src/NzbDrone.Core/Annotations/SelectOption.cs | 10 + .../Annotations/SelectOptionsConverter.cs | 9 + .../CustomFormats/CustomFormat.cs | 71 +++ .../CustomFormatCalculationService.cs | 183 ++++++++ .../CustomFormats/CustomFormatInput.cs | 36 ++ .../CustomFormats/CustomFormatRepository.cs | 17 + .../CustomFormats/CustomFormatService.cs | 77 ++++ .../Events/CustomFormatAddedEvent.cs | 14 + .../Events/CustomFormatDeletedEvent.cs | 14 + .../SpecificationMatchesGroup.cs | 13 + .../CustomFormatSpecificationBase.cs | 36 ++ .../ICustomFormatSpecification.cs | 20 + .../Specifications/RegexSpecificationBase.cs | 54 +++ .../ReleaseGroupSpecification.cs | 14 + .../ReleaseTitleSpecification.cs | 14 + .../Specifications/SizeSpecification.cs | 41 ++ .../Converters/CustomFormatIntConverter.cs | 20 + .../CustomFormatSpecificationConverter.cs | 74 +++ .../Migration/026_add_custom_formats.cs | 304 +++++++++++++ src/NzbDrone.Core/Datastore/TableMapping.cs | 6 + .../DownloadDecisionComparer.cs | 6 +- .../DecisionEngine/DownloadDecisionMaker.cs | 11 + ...stomFormatAllowedByProfileSpecification.cs | 25 + .../Specifications/CutoffSpecification.cs | 14 +- .../Specifications/QueueSpecification.cs | 26 +- .../ReleaseRestrictionsSpecification.cs | 8 +- .../RssSync/DelaySpecification.cs | 28 +- .../RssSync/HistorySpecification.cs | 30 +- .../Specifications/UpgradableSpecification.cs | 57 ++- .../UpgradeAllowedSpecification.cs | 11 +- .../UpgradeDiskSpecification.cs | 24 +- .../AggregatePreferredWordScore.cs | 22 - .../History/DownloadHistoryService.cs | 2 +- .../Download/Pending/PendingReleaseService.cs | 11 + .../TrackedDownloadService.cs | 13 +- .../History/HistoryRepository.cs | 10 +- .../CleanupQualityProfileFormatItems.cs | 93 ++++ src/NzbDrone.Core/Localization/Core/en.json | 11 + src/NzbDrone.Core/MediaFiles/BookFile.cs | 3 +- .../Aggregation/AggregationService.cs | 3 + .../BookImport/ImportDecisionMaker.cs | 6 +- .../BookImport/Manual/ManualImportItem.cs | 3 + .../BookImport/Manual/ManualImportService.cs | 8 +- .../BookImport/SceneNameCalculator.cs | 38 ++ .../Specifications/UpgradeSpecification.cs | 7 +- .../DownloadedBooksImportService.cs | 2 +- .../Notifications/Webhook/WebhookRelease.cs | 6 + .../Organizer/FileNameBuilder.cs | 22 +- .../Organizer/FileNameSampleService.cs | 24 +- src/NzbDrone.Core/Parser/Model/LocalBook.cs | 3 +- .../Parser/Model/ParsedBookInfo.cs | 6 + src/NzbDrone.Core/Parser/Model/RemoteBook.cs | 5 +- src/NzbDrone.Core/Parser/Parser.cs | 9 +- .../Profiles/Delay/DelayProfile.cs | 5 +- .../Profiles/ProfileFormatItem.cs | 11 + .../Profiles/Qualities/QualityProfile.cs | 28 ++ .../Qualities/QualityProfileRepository.cs | 40 +- .../Qualities/QualityProfileService.cs | 55 ++- .../Profiles/Releases/PreferredWordService.cs | 86 ---- .../Profiles/Releases/ReleaseProfile.cs | 10 +- .../Releases/ReleaseProfileService.cs | 8 - .../CaseInsensitiveTermMatcher.cs | 4 +- .../Blocklist/BlocklistController.cs | 8 +- .../Blocklist/BlocklistResource.cs | 6 +- .../CustomFormats/CustomFormatController.cs | 114 +++++ .../CustomFormats/CustomFormatResource.cs | 75 +++ .../CustomFormatSpecificationSchema.cs | 36 ++ .../History/HistoryController.cs | 28 +- src/Readarr.Api.V1/History/HistoryResource.cs | 6 +- .../Indexers/ReleaseResource.cs | 9 +- .../Profiles/Delay/DelayProfileResource.cs | 11 +- .../Quality/QualityProfileController.cs | 36 +- .../Quality/QualityProfileResource.cs | 41 +- .../Release/ReleaseProfileController.cs | 7 +- .../Release/ReleaseProfileResource.cs | 10 +- src/Readarr.Api.V1/Queue/QueueResource.cs | 3 + src/Readarr.Http/ClientSchema/Field.cs | 1 + src/Readarr.Http/REST/RestResource.cs | 2 +- yarn.lock | 67 ++- 185 files changed, 6964 insertions(+), 800 deletions(-) create mode 100644 frontend/src/Book/BookFormats.js create mode 100644 frontend/src/Components/Form/TextArea.css create mode 100644 frontend/src/Components/Form/TextArea.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormatSettingsConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModal.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContentConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModal.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContentConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModal.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContentConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContentConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContentConnector.js create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.css create mode 100644 frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileFormatItem.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileFormatItem.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js create mode 100644 frontend/src/Store/Actions/Settings/customFormatSpecifications.js create mode 100644 frontend/src/Store/Actions/Settings/customFormats.js create mode 100644 frontend/src/Utilities/Number/formatPreferredWordScore.js create mode 100644 frontend/src/Utilities/State/getNextId.js create mode 100644 src/NzbDrone.Core.Test/CustomFormats/CustomFormatsTestHelpers.cs create mode 100644 src/NzbDrone.Core.Test/Datastore/Migration/026_add_custom_formatsFixture.cs create mode 100644 src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs rename src/NzbDrone.Core.Test/DecisionEngineTests/{ => RssSync}/HistorySpecificationFixture.cs (93%) create mode 100644 src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupQualityProfileFormatItemsFixture.cs create mode 100644 src/NzbDrone.Core.Test/MediaFiles/TrackImport/GetSceneNameFixture.cs delete mode 100644 src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/SameFileSpecificationFixture.cs rename src/NzbDrone.Core.Test/Profiles/{ProfileServiceFixture.cs => QualityProfileServiceFixture.cs} (95%) delete mode 100644 src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs delete mode 100644 src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/GetMatchingPreferredWordsFixture.cs create mode 100644 src/NzbDrone.Core/Annotations/SelectOption.cs create mode 100644 src/NzbDrone.Core/Annotations/SelectOptionsConverter.cs create mode 100644 src/NzbDrone.Core/CustomFormats/CustomFormat.cs create mode 100644 src/NzbDrone.Core/CustomFormats/CustomFormatCalculationService.cs create mode 100644 src/NzbDrone.Core/CustomFormats/CustomFormatInput.cs create mode 100644 src/NzbDrone.Core/CustomFormats/CustomFormatRepository.cs create mode 100644 src/NzbDrone.Core/CustomFormats/CustomFormatService.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Events/CustomFormatAddedEvent.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Events/CustomFormatDeletedEvent.cs create mode 100644 src/NzbDrone.Core/CustomFormats/SpecificationMatchesGroup.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/CustomFormatSpecificationBase.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/ICustomFormatSpecification.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/RegexSpecificationBase.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/ReleaseGroupSpecification.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/ReleaseTitleSpecification.cs create mode 100644 src/NzbDrone.Core/CustomFormats/Specifications/SizeSpecification.cs create mode 100644 src/NzbDrone.Core/Datastore/Converters/CustomFormatIntConverter.cs create mode 100644 src/NzbDrone.Core/Datastore/Converters/CustomFormatSpecificationConverter.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/026_add_custom_formats.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/CustomFormatAllowedByProfileSpecification.cs delete mode 100644 src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs create mode 100644 src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs create mode 100644 src/NzbDrone.Core/MediaFiles/BookImport/SceneNameCalculator.cs create mode 100644 src/NzbDrone.Core/Profiles/ProfileFormatItem.cs delete mode 100644 src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs create mode 100644 src/Readarr.Api.V1/CustomFormats/CustomFormatController.cs create mode 100644 src/Readarr.Api.V1/CustomFormats/CustomFormatResource.cs create mode 100644 src/Readarr.Api.V1/CustomFormats/CustomFormatSpecificationSchema.cs diff --git a/frontend/build/webpack.config.js b/frontend/build/webpack.config.js index 6f324ac95..1c86e39d4 100644 --- a/frontend/build/webpack.config.js +++ b/frontend/build/webpack.config.js @@ -44,7 +44,8 @@ module.exports = (env) => { 'node_modules' ], alias: { - jquery: 'jquery/src/jquery' + jquery: 'jquery/src/jquery', + 'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate' }, fallback: { buffer: false, diff --git a/frontend/src/Activity/Blocklist/BlocklistRow.js b/frontend/src/Activity/Blocklist/BlocklistRow.js index 2df5e803d..f91688cd5 100644 --- a/frontend/src/Activity/Blocklist/BlocklistRow.js +++ b/frontend/src/Activity/Blocklist/BlocklistRow.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import AuthorNameLink from 'Author/AuthorNameLink'; +import BookFormats from 'Book/BookFormats'; import BookQuality from 'Book/BookQuality'; import IconButton from 'Components/Link/IconButton'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; @@ -45,6 +46,7 @@ class BlocklistRow extends Component { author, sourceTitle, quality, + customFormats, date, protocol, indexer, @@ -110,6 +112,16 @@ class BlocklistRow extends Component { ); } + if (name === 'customFormats') { + return ( + + + + ); + } + if (name === 'date') { return ( : + null + } + + { + nzbInfoUrl ? Info URL @@ -109,7 +120,8 @@ function HistoryDetails(props) { {nzbInfoUrl} - + : + null } { @@ -173,6 +185,7 @@ function HistoryDetails(props) { if (eventType === 'bookFileImported') { const { + customFormatScore, droppedPath, importedPath } = data; @@ -195,12 +208,22 @@ function HistoryDetails(props) { } { - !!importedPath && + importedPath ? + /> : + null + } + + { + customFormatScore && customFormatScore !== '0' ? + : + null } ); @@ -208,7 +231,8 @@ function HistoryDetails(props) { if (eventType === 'bookFileDeleted') { const { - reason + reason, + customFormatScore } = data; let reasonMessage = ''; @@ -238,6 +262,15 @@ function HistoryDetails(props) { title={translate('Reason')} data={reasonMessage} /> + + { + customFormatScore && customFormatScore !== '0' ? + : + null + } ); } diff --git a/frontend/src/Activity/History/HistoryRow.css b/frontend/src/Activity/History/HistoryRow.css index 669377fdb..039804b63 100644 --- a/frontend/src/Activity/History/HistoryRow.css +++ b/frontend/src/Activity/History/HistoryRow.css @@ -10,6 +10,12 @@ width: 80px; } +.customFormatScore { + composes: cell from '~Components/Table/Cells/TableRowCell.css'; + + width: 55px; +} + .releaseGroup { composes: cell from '~Components/Table/Cells/TableRowCell.css'; diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js index 99542e861..2a0465e35 100644 --- a/frontend/src/Activity/History/HistoryRow.js +++ b/frontend/src/Activity/History/HistoryRow.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import AuthorNameLink from 'Author/AuthorNameLink'; +import BookFormats from 'Book/BookFormats'; import BookQuality from 'Book/BookQuality'; import BookTitleLink from 'Book/BookTitleLink'; import IconButton from 'Components/Link/IconButton'; @@ -8,6 +9,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRow from 'Components/Table/TableRow'; import { icons } from 'Helpers/Props'; +import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import HistoryDetailsModal from './Details/HistoryDetailsModal'; import HistoryEventTypeCell from './HistoryEventTypeCell'; import styles from './HistoryRow.css'; @@ -54,6 +56,7 @@ class HistoryRow extends Component { author, book, quality, + customFormats, qualityCutoffNotMet, eventType, sourceTitle, @@ -127,6 +130,16 @@ class HistoryRow extends Component { ); } + if (name === 'customFormats') { + return ( + + + + ); + } + if (name === 'date') { return ( + {formatPreferredWordScore(data.customFormatScore)} + + ); + } + if (name === 'releaseGroup') { return ( + + + ); + } + if (name === 'protocol') { return ( @@ -379,6 +391,7 @@ QueueRow.propTypes = { author: PropTypes.object, book: PropTypes.object, quality: PropTypes.object.isRequired, + customFormats: PropTypes.arrayOf(PropTypes.object), protocol: PropTypes.string.isRequired, indexer: PropTypes.string, outputPath: PropTypes.string, diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index cc68d66c7..41d75375b 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -13,6 +13,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector'; import NotFound from 'Components/NotFound'; import Switch from 'Components/Router/Switch'; import AddNewItemConnector from 'Search/AddNewItemConnector'; +import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector'; import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; @@ -175,6 +176,11 @@ function AppRoutes(props) { component={QualityConnector} /> + + + { + formats.map((format) => { + return ( + + ); + }) + } + + ); +} + +BookFormats.propTypes = { + formats: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +BookFormats.defaultProps = { + formats: [] +}; + +export default BookFormats; diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index 0e6b62690..7306b4bcf 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -25,6 +25,7 @@ import PathInputConnector from './PathInputConnector'; import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector'; import RootFolderSelectInputConnector from './RootFolderSelectInputConnector'; import TagInputConnector from './TagInputConnector'; +import TextArea from './TextArea'; import TextInput from './TextInput'; import TextTagInputConnector from './TextTagInputConnector'; import UMaskInput from './UMaskInput'; @@ -92,6 +93,9 @@ function getComponent(type) { case inputTypes.TAG: return TagInputConnector; + case inputTypes.TEXT_AREA: + return TextArea; + case inputTypes.TEXT_TAG: return TextTagInputConnector; diff --git a/frontend/src/Components/Form/TextArea.css b/frontend/src/Components/Form/TextArea.css new file mode 100644 index 000000000..7a4961c07 --- /dev/null +++ b/frontend/src/Components/Form/TextArea.css @@ -0,0 +1,19 @@ +.input { + composes: input from '~Components/Form/Input.css'; + + flex-grow: 1; + min-height: 200px; + resize: vertical; +} + +.readOnly { + background-color: #eee; +} + +.hasError { + composes: hasError from '~Components/Form/Input.css'; +} + +.hasWarning { + composes: hasWarning from '~Components/Form/Input.css'; +} diff --git a/frontend/src/Components/Form/TextArea.js b/frontend/src/Components/Form/TextArea.js new file mode 100644 index 000000000..44fd3a249 --- /dev/null +++ b/frontend/src/Components/Form/TextArea.js @@ -0,0 +1,172 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './TextArea.css'; + +class TextArea extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._input = null; + this._selectionStart = null; + this._selectionEnd = null; + this._selectionTimeout = null; + this._isMouseTarget = false; + } + + componentDidMount() { + window.addEventListener('mouseup', this.onDocumentMouseUp); + } + + componentWillUnmount() { + window.removeEventListener('mouseup', this.onDocumentMouseUp); + + if (this._selectionTimeout) { + this._selectionTimeout = clearTimeout(this._selectionTimeout); + } + } + + // + // Control + + setInputRef = (ref) => { + this._input = ref; + }; + + selectionChange() { + if (this._selectionTimeout) { + this._selectionTimeout = clearTimeout(this._selectionTimeout); + } + + this._selectionTimeout = setTimeout(() => { + const selectionStart = this._input.selectionStart; + const selectionEnd = this._input.selectionEnd; + + const selectionChanged = ( + this._selectionStart !== selectionStart || + this._selectionEnd !== selectionEnd + ); + + this._selectionStart = selectionStart; + this._selectionEnd = selectionEnd; + + if (this.props.onSelectionChange && selectionChanged) { + this.props.onSelectionChange(selectionStart, selectionEnd); + } + }, 10); + } + + // + // Listeners + + onChange = (event) => { + const { + name, + onChange + } = this.props; + + const payload = { + name, + value: event.target.value + }; + + onChange(payload); + }; + + onFocus = (event) => { + if (this.props.onFocus) { + this.props.onFocus(event); + } + + this.selectionChange(); + }; + + onKeyUp = () => { + this.selectionChange(); + }; + + onMouseDown = () => { + this._isMouseTarget = true; + }; + + onMouseUp = () => { + this.selectionChange(); + }; + + onDocumentMouseUp = () => { + if (this._isMouseTarget) { + this.selectionChange(); + } + + this._isMouseTarget = false; + }; + + // + // Render + + render() { + const { + className, + readOnly, + autoFocus, + placeholder, + name, + value, + hasError, + hasWarning, + onBlur + } = this.props; + + return ( +