mirror of
https://github.com/fergalmoran/Readarr.git
synced 2026-01-06 08:46:34 +00:00
New: Search bar searches books as well as authors
This commit is contained in:
@@ -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
|
||||
|
||||
39
frontend/src/Components/Page/Header/BookSearchResult.css
Normal file
39
frontend/src/Components/Page/Header/BookSearchResult.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
39
frontend/src/Components/Page/Header/BookSearchResult.js
Normal file
39
frontend/src/Components/Page/Header/BookSearchResult.js
Normal file
@@ -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\"",
|
||||
|
||||
Reference in New Issue
Block a user