diff --git a/frontend/gulp/webpack.js b/frontend/gulp/webpack.js index d02d8cdb9..fb33a7afd 100644 --- a/frontend/gulp/webpack.js +++ b/frontend/gulp/webpack.js @@ -116,6 +116,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/Loading/LoadingIndicator.js b/frontend/src/Components/Loading/LoadingIndicator.js index 5f9a15b1a..60f692a45 100644 --- a/frontend/src/Components/Loading/LoadingIndicator.js +++ b/frontend/src/Components/Loading/LoadingIndicator.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import styles from './LoadingIndicator.css'; -function LoadingIndicator({ className, size }) { +function LoadingIndicator({ className, rippleClassName, size }) { const sizeInPx = `${size}px`; const width = sizeInPx; const height = sizeInPx; @@ -17,17 +17,17 @@ function LoadingIndicator({ className, size }) { style={{ width, height }} >
@@ -37,11 +37,13 @@ function LoadingIndicator({ className, size }) { LoadingIndicator.propTypes = { className: PropTypes.string, + rippleClassName: PropTypes.string, size: PropTypes.number }; LoadingIndicator.defaultProps = { className: styles.loading, + rippleClassName: styles.ripple, size: 50 }; diff --git a/frontend/src/Components/Page/Header/SeriesSearchInput.css b/frontend/src/Components/Page/Header/SeriesSearchInput.css index 34d87cda9..9927f1013 100644 --- a/frontend/src/Components/Page/Header/SeriesSearchInput.css +++ b/frontend/src/Components/Page/Header/SeriesSearchInput.css @@ -3,6 +3,18 @@ align-items: center; } +.loading { + margin-top: 18px; + margin-bottom: 18px; + text-align: center; +} + +.ripple { + composes: ripple from '~Components/Loading/LoadingIndicator.css'; + + border: 2px solid $toolbarColor; +} + .input { margin-left: 8px; width: 200px; diff --git a/frontend/src/Components/Page/Header/SeriesSearchInput.js b/frontend/src/Components/Page/Header/SeriesSearchInput.js index d04e3cc07..0bd5178c8 100644 --- a/frontend/src/Components/Page/Header/SeriesSearchInput.js +++ b/frontend/src/Components/Page/Header/SeriesSearchInput.js @@ -1,29 +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 SeriesSearchResult from './SeriesSearchResult'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FuseWorker from './fuse.worker'; import styles from './SeriesSearchInput.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: [ - 'title', - 'alternateTitles.title', - 'tags.label' - ] -}; +const workerInstance = new FuseWorker(); class SeriesSearchInput extends Component { @@ -43,6 +32,7 @@ class SeriesSearchInput extends Component { componentDidMount() { this.props.bindShortcut(shortcuts.SERIES_SEARCH_INPUT.key, this.focusInput); + workerInstance.addEventListener('message', this.onSuggestionsReceived, false); } // @@ -82,6 +72,16 @@ class SeriesSearchInput extends Component { ); } + if (item.type === LOADING_TYPE) { + return ( + + ); + } + return ( { - const { series } = this.props; - let suggestions = []; - - if (value.length === 1) { - suggestions = series.reduce((acc, s) => { - if (s.firstCharacter === value.toLowerCase()) { - acc.push({ - item: s, - indices: [ - [0, 0] - ], - matches: [ - { - value: s.title, - key: 'title' - } - ], - arrayIndex: 0 - }); + this.setState({ + suggestions: [ + { + type: LOADING_TYPE, + title: value } + ] + }); + this.requestSuggestions(value); + }; - return acc; - }, []); - } else { - const fuse = new Fuse(series, fuseOptions); - suggestions = fuse.search(value); - } + requestSuggestions = _.debounce((value) => { + const payload = { + value, + series: this.props.series + }; + + workerInstance.postMessage(payload); + }, 250); - this.setState({ suggestions }); + 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..708eaf8e0 --- /dev/null +++ b/frontend/src/Components/Page/Header/fuse.worker.js @@ -0,0 +1,63 @@ +import Fuse from 'fuse.js'; + +const fuseOptions = { + shouldSort: true, + includeMatches: true, + threshold: 0.3, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + 'title', + 'alternateTitles.title', + 'tags.label' + ] +}; + +function getSuggestions(series, value) { + const limit = 10; + let suggestions = []; + + if (value.length === 1) { + for (let i = 0; i < series.length; i++) { + const s = series[i]; + if (s.firstCharacter === value.toLowerCase()) { + suggestions.push({ + item: series[i], + indices: [ + [0, 0] + ], + matches: [ + { + value: s.title, + key: 'title' + } + ], + arrayIndex: 0 + }); + if (suggestions.length > limit) { + break; + } + } + } + } else { + const fuse = new Fuse(series, fuseOptions); + suggestions = fuse.search(value, { limit }); + } + + return suggestions; +} + +self.addEventListener('message', (e) => { + if (!e) { + return; + } + + const { + series, + value + } = e.data; + + self.postMessage(getSuggestions(series, value)); +}); diff --git a/frontend/src/index.js b/frontend/src/index.js index 275e3ff44..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.Sonarr.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..e74b4f1be --- /dev/null +++ b/frontend/src/preload.js @@ -0,0 +1,2 @@ +/* eslint no-undef: 0 */ +__webpack_public_path__ = `${window.Sonarr.urlBase}/`; diff --git a/package.json b/package.json index 1d967ab4d..b5e9dcd8b 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,8 @@ "stylelint-order": "3.0.1", "url-loader": "2.0.1", "webpack": "4.35.3", - "webpack-stream": "5.2.1" + "webpack-stream": "5.2.1", + "worker-loader": "2.0.0" }, "main": "index.js", "browserslist": [ diff --git a/yarn.lock b/yarn.lock index 6ce5ea4b5..255e28f55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5325,7 +5325,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.2, loader-utils@^1.2.3: +loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.2, 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== @@ -8049,6 +8049,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" @@ -9571,6 +9579,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"