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 ( +