diff --git a/frontend/src/Components/Page/Header/AuthorSearchInput.js b/frontend/src/Components/Page/Header/AuthorSearchInput.js index 0b6f33d5d..7c767e6e7 100644 --- a/frontend/src/Components/Page/Header/AuthorSearchInput.js +++ b/frontend/src/Components/Page/Header/AuthorSearchInput.js @@ -6,7 +6,9 @@ import Icon from 'Components/Icon'; import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import { icons } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; import AuthorSearchResult from './AuthorSearchResult'; +import BookSearchResult from './BookSearchResult'; import FuseWorker from './fuse.worker'; import styles from './AuthorSearchInput.css'; @@ -96,17 +98,43 @@ class AuthorSearchInput extends Component { ); } - return ( - - ); + if (item.item.type === 'author') { + return ( + + ); + } + + if (item.item.type === 'book') { + return ( + + ); + } } - goToAuthor(item) { + goToItem(item) { + const { + onGoToAuthor, + onGoToBook + } = this.props; + this.setState({ value: '' }); - this.props.onGoToAuthor(item.item.titleSlug); + + const { + type, + titleSlug + } = item.item; + + if (type === 'author') { + onGoToAuthor(titleSlug); + } else if (type === 'book') { + onGoToBook(titleSlug); + } } reset() { @@ -164,9 +192,9 @@ class AuthorSearchInput extends Component { // otherwise go to the selected author. if (highlightedSuggestionIndex == null) { - this.goToAuthor(suggestions[0]); + this.goToItem(suggestions[0]); } else { - this.goToAuthor(suggestions[highlightedSuggestionIndex]); + this.goToItem(suggestions[highlightedSuggestionIndex]); } this._autosuggest.input.blur(); @@ -202,7 +230,7 @@ class AuthorSearchInput extends Component { if (!requestLoading) { const payload = { value, - authors: this.props.authors + items: this.props.items }; this.getWorker().postMessage(payload); @@ -235,7 +263,7 @@ class AuthorSearchInput extends Component { const payload = { value: this.state.requestValue, - authors: this.props.authors + items: this.props.items }; this.getWorker().postMessage(payload); @@ -253,7 +281,7 @@ class AuthorSearchInput extends Component { if (suggestion.type === ADD_NEW_TYPE) { this.props.onGoToAddNewAuthor(this.state.value); } else { - this.goToAuthor(suggestion); + this.goToItem(suggestion); } } @@ -271,14 +299,14 @@ class AuthorSearchInput extends Component { if (suggestions.length || loading) { suggestionGroups.push({ - title: 'Existing Author', + title: translate('ExistingItems'), loading, suggestions }); } suggestionGroups.push({ - title: 'Add New Item', + title: translate('AddNewItem'), suggestions: [ { type: ADD_NEW_TYPE, @@ -292,7 +320,7 @@ class AuthorSearchInput extends Component { className: styles.input, name: 'authorSearch', value, - placeholder: 'Search', + placeholder: translate('Search'), autoComplete: 'off', spellCheck: false, onChange: this.onChange, @@ -336,8 +364,9 @@ class AuthorSearchInput extends Component { } AuthorSearchInput.propTypes = { - authors: PropTypes.arrayOf(PropTypes.object).isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, onGoToAuthor: PropTypes.func.isRequired, + onGoToBook: PropTypes.func.isRequired, onGoToAddNewAuthor: PropTypes.func.isRequired, bindShortcut: PropTypes.func.isRequired }; diff --git a/frontend/src/Components/Page/Header/AuthorSearchInputConnector.js b/frontend/src/Components/Page/Header/AuthorSearchInputConnector.js index 50b10e450..0b244db02 100644 --- a/frontend/src/Components/Page/Header/AuthorSearchInputConnector.js +++ b/frontend/src/Components/Page/Header/AuthorSearchInputConnector.js @@ -21,7 +21,8 @@ function createCleanAuthorSelector() { } = author; return { - authorName, + type: 'author', + name: authorName, sortName, titleSlug, images, @@ -40,12 +41,41 @@ function createCleanAuthorSelector() { ); } +function createCleanBookSelector() { + return createSelector( + (state) => state.books.items, + (allBooks) => { + return allBooks.map((book) => { + const { + title, + images, + titleSlug + } = book; + + return { + type: 'book', + name: title, + sortName: title, + titleSlug, + images, + tags: [] + }; + }); + } + ); +} + function createMapStateToProps() { return createDeepEqualSelector( createCleanAuthorSelector(), - (authors) => { + createCleanBookSelector(), + (authors, books) => { + const items = [ + ...authors, + ...books + ]; return { - authors + items }; } ); @@ -57,6 +87,10 @@ function createMapDispatchToProps(dispatch, props) { dispatch(push(`${window.Readarr.urlBase}/author/${titleSlug}`)); }, + onGoToBook(titleSlug) { + dispatch(push(`${window.Readarr.urlBase}/book/${titleSlug}`)); + }, + onGoToAddNewAuthor(query) { dispatch(push(`${window.Readarr.urlBase}/add/search?term=${encodeURIComponent(query)}`)); } diff --git a/frontend/src/Components/Page/Header/AuthorSearchResult.js b/frontend/src/Components/Page/Header/AuthorSearchResult.js index 2b5a41293..2fb7a6457 100644 --- a/frontend/src/Components/Page/Header/AuthorSearchResult.js +++ b/frontend/src/Components/Page/Header/AuthorSearchResult.js @@ -8,7 +8,7 @@ import styles from './AuthorSearchResult.css'; function AuthorSearchResult(props) { const { match, - authorName, + name, images, tags } = props; @@ -31,7 +31,7 @@ function AuthorSearchResult(props) {
- {authorName} + {name}
{ @@ -52,7 +52,7 @@ function AuthorSearchResult(props) { } AuthorSearchResult.propTypes = { - authorName: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, tags: PropTypes.arrayOf(PropTypes.object).isRequired, match: PropTypes.object.isRequired diff --git a/frontend/src/Components/Page/Header/BookSearchResult.css b/frontend/src/Components/Page/Header/BookSearchResult.css new file mode 100644 index 000000000..4fba173b6 --- /dev/null +++ b/frontend/src/Components/Page/Header/BookSearchResult.css @@ -0,0 +1,39 @@ +.result { + display: flex; + padding: 3px; + cursor: pointer; +} + +.poster { + width: 35px; + height: 35px; + object-fit: contain; +} + +.titles { + flex: 1 1 1px; +} + +.title { + flex: 1 1 1px; + margin-left: 5px; +} + +.alternateTitle { + composes: title; + + color: $disabledColor; + font-size: $smallFontSize; +} + +.tagContainer { + composes: title; +} + +@media only screen and (max-width: $breakpointSmall) { + .titles, + .title, + .alternateTitle { + @add-mixin truncate; + } +} diff --git a/frontend/src/Components/Page/Header/BookSearchResult.js b/frontend/src/Components/Page/Header/BookSearchResult.js new file mode 100644 index 000000000..4fc427865 --- /dev/null +++ b/frontend/src/Components/Page/Header/BookSearchResult.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import AuthorPoster from 'Author/AuthorPoster'; +import styles from './BookSearchResult.css'; + +function BookSearchResult(props) { + const { + name, + images + } = props; + + return ( +
+ + +
+
+ {name} +
+
+
+ ); +} + +BookSearchResult.propTypes = { + name: PropTypes.string.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + tags: PropTypes.arrayOf(PropTypes.object).isRequired, + match: PropTypes.object.isRequired +}; + +export default BookSearchResult; diff --git a/frontend/src/Components/Page/Header/fuse.worker.js b/frontend/src/Components/Page/Header/fuse.worker.js index 8d0f06c60..5ddb505f4 100644 --- a/frontend/src/Components/Page/Header/fuse.worker.js +++ b/frontend/src/Components/Page/Header/fuse.worker.js @@ -8,43 +8,16 @@ const fuseOptions = { distance: 100, minMatchCharLength: 1, keys: [ - 'authorName', + 'name', 'tags.label' ] }; -function getSuggestions(authors, value) { +function getSuggestions(items, value) { const limit = 10; - let suggestions = []; - if (value.length === 1) { - for (let i = 0; i < authors.length; i++) { - const s = authors[i]; - if (s.firstCharacter === value.toLowerCase()) { - suggestions.push({ - item: authors[i], - indices: [ - [0, 0] - ], - matches: [ - { - value: s.title, - key: 'title' - } - ], - arrayIndex: 0 - }); - if (suggestions.length > limit) { - break; - } - } - } - } else { - const fuse = new Fuse(authors, fuseOptions); - suggestions = fuse.search(value, { limit }); - } - - return suggestions; + const fuse = new Fuse(items, fuseOptions); + return fuse.search(value, { limit }); } onmessage = function(e) { @@ -53,16 +26,20 @@ onmessage = function(e) { } const { - authors, + items, value } = e.data; - const suggestions = getSuggestions(authors, value); + console.log(`got search request ${value} with ${items.length} items`); + + const suggestions = getSuggestions(items, value); const results = { value, suggestions }; + console.log(`return ${suggestions.length} results for search ${value}`); + self.postMessage(results); }; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 507a8b0b6..0d3ebd1b3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -217,6 +217,7 @@ "ErrorLoadingPreviews": "Error loading previews", "Exception": "Exception", "ExistingBooks": "Existing Books", + "ExistingItems": "Existing Items", "ExistingTagsScrubbed": "Existing tags scrubbed", "ExtraFileExtensionsHelpTexts1": "Comma separated list of extra files to import (.nfo will be imported as .nfo-orig)", "ExtraFileExtensionsHelpTexts2": "Examples: \".sub, .nfo\" or \"sub,nfo\"",