From cccac3532dd9d0fba2cf1fd96e47692bf5967076 Mon Sep 17 00:00:00 2001 From: ta264 Date: Mon, 9 Dec 2019 22:02:02 +0000 Subject: [PATCH] Fixed: Use a worker for UI fuzzy search --- frontend/gulp/webpack.js | 9 +++ .../Page/Header/ArtistSearchInput.js | 61 +++++++++++------- .../src/Components/Page/Header/fuse.worker.js | 62 +++++++++++++++++++ frontend/src/index.js | 4 +- frontend/src/preload.js | 2 + package.json | 3 +- yarn.lock | 18 +++++- 7 files changed, 132 insertions(+), 27 deletions(-) create mode 100644 frontend/src/Components/Page/Header/fuse.worker.js create mode 100644 frontend/src/preload.js diff --git a/frontend/gulp/webpack.js b/frontend/gulp/webpack.js index 7294f22f1..56b222935 100644 --- a/frontend/gulp/webpack.js +++ b/frontend/gulp/webpack.js @@ -119,6 +119,15 @@ const config = { module: { rules: [ + { + test: /\.worker\.js$/, + use: { + loader: 'worker-loader', + options: { + name: '[name].js' + } + } + }, { test: /\.js?$/, exclude: /(node_modules|JsLibraries)/, diff --git a/frontend/src/Components/Page/Header/ArtistSearchInput.js b/frontend/src/Components/Page/Header/ArtistSearchInput.js index eb22640ce..335375a60 100644 --- a/frontend/src/Components/Page/Header/ArtistSearchInput.js +++ b/frontend/src/Components/Page/Header/ArtistSearchInput.js @@ -1,28 +1,18 @@ +import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Autosuggest from 'react-autosuggest'; -import Fuse from 'fuse.js'; import { icons } from 'Helpers/Props'; import Icon from 'Components/Icon'; import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; import ArtistSearchResult from './ArtistSearchResult'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FuseWorker from './fuse.worker'; import styles from './ArtistSearchInput.css'; +const LOADING_TYPE = 'suggestionsLoading'; const ADD_NEW_TYPE = 'addNew'; - -const fuseOptions = { - shouldSort: true, - includeMatches: true, - threshold: 0.3, - location: 0, - distance: 100, - maxPatternLength: 32, - minMatchCharLength: 1, - keys: [ - 'artistName', - 'tags.label' - ] -}; +const workerInstance = new FuseWorker(); class ArtistSearchInput extends Component { @@ -42,6 +32,7 @@ class ArtistSearchInput extends Component { componentDidMount() { this.props.bindShortcut(shortcuts.ARTIST_SEARCH_INPUT.key, this.focusInput); + workerInstance.addEventListener('message', this.onSuggestionsReceived, false); } // @@ -69,7 +60,7 @@ class ArtistSearchInput extends Component { } getSuggestionValue({ title }) { - return title || ''; + return title; } renderSuggestion(item, { query }) { @@ -81,6 +72,12 @@ class ArtistSearchInput extends Component { ); } + if (item.type === LOADING_TYPE) { + return ( + + ); + } + return ( { - if (event.key !== 'Tab' && event.key !== 'Enter' || event.key !== 'ArrowDown' || event.key !== 'ArrowUp') { + if (event.key !== 'Tab' && event.key !== 'Enter') { return; } @@ -127,7 +124,7 @@ class ArtistSearchInput extends Component { highlightedSuggestionIndex } = this._autosuggest.state; - if (!suggestions.length || highlightedSectionIndex && (event.key !== 'ArrowDown' || event.key !== 'ArrowUp')) { + if (!suggestions.length || suggestions[0].type === LOADING_TYPE || highlightedSectionIndex) { this.props.onGoToAddNewArtist(value); this._autosuggest.input.blur(); this.reset(); @@ -138,7 +135,7 @@ class ArtistSearchInput extends Component { // If an suggestion is not selected go to the first artist, // otherwise go to the selected artist. - if (highlightedSuggestionIndex == null && (event.key !== 'ArrowDown' || event.key !== 'ArrowUp')) { + if (highlightedSuggestionIndex == null) { this.goToArtist(suggestions[0]); } else { this.goToArtist(suggestions[highlightedSuggestionIndex]); @@ -153,10 +150,30 @@ class ArtistSearchInput extends Component { } onSuggestionsFetchRequested = ({ value }) => { - const fuse = new Fuse(this.props.artists, fuseOptions); - const suggestions = fuse.search(value).slice(0, 15); + this.setState({ + suggestions: [ + { + type: LOADING_TYPE, + title: value + } + ] + }); + this.requestSuggestions(value); + }; - this.setState({ suggestions }); + requestSuggestions = _.debounce((value) => { + const payload = { + value, + artists: this.props.artists + }; + + workerInstance.postMessage(payload); + }, 250); + + onSuggestionsReceived = (message) => { + this.setState({ + suggestions: message.data + }); } onSuggestionsClearRequested = () => { diff --git a/frontend/src/Components/Page/Header/fuse.worker.js b/frontend/src/Components/Page/Header/fuse.worker.js new file mode 100644 index 000000000..8064a408a --- /dev/null +++ b/frontend/src/Components/Page/Header/fuse.worker.js @@ -0,0 +1,62 @@ +import Fuse from 'fuse.js'; + +const fuseOptions = { + shouldSort: true, + includeMatches: true, + threshold: 0.3, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + 'artistName', + 'tags.label' + ] +}; + +function getSuggestions(artists, value) { + const limit = 10; + let suggestions = []; + + if (value.length === 1) { + for (let i = 0; i < artists.length; i++) { + const s = artists[i]; + if (s.firstCharacter === value.toLowerCase()) { + suggestions.push({ + item: artists[i], + indices: [ + [0, 0] + ], + matches: [ + { + value: s.title, + key: 'title' + } + ], + arrayIndex: 0 + }); + if (suggestions.length > limit) { + break; + } + } + } + } else { + const fuse = new Fuse(artists, fuseOptions); + suggestions = fuse.search(value, { limit }); + } + + return suggestions; +} + +self.addEventListener('message', (e) => { + if (!e) { + return; + } + + const { + artists, + value + } = e.data; + + self.postMessage(getSuggestions(artists, value)); +}); diff --git a/frontend/src/index.js b/frontend/src/index.js index 91cfadb98..015aeee0a 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,6 +1,4 @@ -/* eslint-disable-next-line no-undef */ -__webpack_public_path__ = `${window.Lidarr.urlBase}/`; - +import './preload.js'; import React from 'react'; import { render } from 'react-dom'; import { createBrowserHistory } from 'history'; diff --git a/frontend/src/preload.js b/frontend/src/preload.js new file mode 100644 index 000000000..aec17072e --- /dev/null +++ b/frontend/src/preload.js @@ -0,0 +1,2 @@ +/* eslint no-undef: 0 */ +__webpack_public_path__ = `${window.Lidarr.urlBase}/`; diff --git a/package.json b/package.json index b7727ebde..11d67ab1b 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,8 @@ "uglifyjs-webpack-plugin": "2.2.0", "url-loader": "2.1.0", "webpack": "4.39.3", - "webpack-stream": "5.2.1" + "webpack-stream": "5.2.1", + "worker-loader": "2.0.0" }, "devDependencies": { "@sentry/cli": "1.47.1" diff --git a/yarn.lock b/yarn.lock index 77a755cc1..344484abd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5541,7 +5541,7 @@ loader-utils@^0.2.16: json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: +loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== @@ -8422,6 +8422,14 @@ scheduler@^0.13.6: loose-envify "^1.1.0" object-assign "^4.1.1" +schema-utils@^0.4.0: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -9980,6 +9988,14 @@ worker-farm@^1.3.1, worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-loader@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac" + integrity sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw== + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.4.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"