New: Search bar searches books as well as authors

pull/1205/head
ta264 3 years ago
parent ba9f618405
commit c9cb0a9774

@ -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 (
<AuthorSearchResult
{...item.item}
match={item.matches[0]}
/>
);
if (item.item.type === 'author') {
return (
<AuthorSearchResult
{...item.item}
match={item.matches[0]}
/>
);
}
if (item.item.type === 'book') {
return (
<BookSearchResult
{...item.item}
match={item.matches[0]}
/>
);
}
}
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
};

@ -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)}`));
}

@ -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) {
<div className={styles.titles}>
<div className={styles.title}>
{authorName}
{name}
</div>
{
@ -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

@ -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;
}
}

@ -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 (
<div className={styles.result}>
<AuthorPoster
className={styles.poster}
images={images}
coverType={'cover'}
size={250}
lazy={false}
overflow={true}
/>
<div className={styles.titles}>
<div className={styles.title}>
{name}
</div>
</div>
</div>
);
}
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;

@ -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);
};

@ -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\"",

Loading…
Cancel
Save