diff --git a/frontend/gulp/webpack.js b/frontend/gulp/webpack.js index 401fc64f7..fb33a7afd 100644 --- a/frontend/gulp/webpack.js +++ b/frontend/gulp/webpack.js @@ -5,14 +5,21 @@ const path = require('path'); const webpack = require('webpack'); const errorHandler = require('./helpers/errorHandler'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); const uiFolder = 'UI'; const frontendFolder = path.join(__dirname, '..'); const srcFolder = path.join(frontendFolder, 'src'); const isProduction = process.argv.indexOf('--production') > -1; +const isProfiling = isProduction && process.argv.indexOf('--profile') > -1; + +const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder); console.log('Source Folder:', srcFolder); +console.log('Output Folder:', distFolder); console.log('isProduction:', isProduction); +console.log('isProfiling:', isProduction); const cssVarsFiles = [ '../src/Styles/Variables/colors', @@ -22,6 +29,22 @@ const cssVarsFiles = [ '../src/Styles/Variables/zIndexes' ].map(require.resolve); +// Override the way HtmlWebpackPlugin injects the scripts +HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) { + const head = assetTags.head.map((v) => { + v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` }; + return this.createHtmlTag(v); + }); + const body = assetTags.body.map((v) => { + v.attributes = { src: `/${v.attributes.src}` }; + return this.createHtmlTag(v); + }); + + return html + .replace('', head.join('\r\n ')) + .replace('', body.join('\r\n ')); +}; + const plugins = [ new webpack.DefinePlugin({ __DEV__: !isProduction, @@ -29,7 +52,12 @@ const plugins = [ }), new MiniCssExtractPlugin({ - filename: path.join('_output', uiFolder, 'Content', 'styles.css') + filename: path.join('Content', 'styles.css') + }), + + new HtmlWebpackPlugin({ + template: 'frontend/src/index.html', + filename: 'index.html' }) ]; @@ -46,8 +74,6 @@ const config = { }, entry: { - preload: 'preload.js', - vendor: 'vendor.js', index: 'index.js' }, @@ -63,12 +89,20 @@ const config = { }, output: { - filename: path.join('_output', uiFolder, '[name].js'), + path: distFolder, + filename: '[name].js', sourceMapFilename: '[file].map' }, optimization: { - chunkIds: 'named' + chunkIds: 'named', + splitChunks: { + chunks: 'initial' + } + }, + + performance: { + hints: false }, plugins, @@ -82,6 +116,15 @@ const config = { module: { rules: [ + { + test: /\.worker\.js$/, + use: { + loader: 'worker-loader', + options: { + name: '[name].js' + } + } + }, { test: /\.js?$/, exclude: /(node_modules|JsLibraries)/, @@ -182,9 +225,27 @@ const config = { } }; +if (isProfiling) { + config.resolve.alias['react-dom$'] = 'react-dom/profiling'; + config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling'; + + config.optimization.minimizer = [ + new TerserPlugin({ + cache: true, + parallel: true, + sourceMap: true, // Must be set to true if using source-maps in production + terserOptions: { + mangle: false, + keep_classnames: true, + keep_fnames: true + } + }) + ]; +} + gulp.task('webpack', () => { return webpackStream(config) - .pipe(gulp.dest('./')); + .pipe(gulp.dest('_output/UI')); }); gulp.task('webpackWatch', () => { @@ -192,7 +253,7 @@ gulp.task('webpackWatch', () => { return webpackStream(config) .on('error', errorHandler) - .pipe(gulp.dest('./')) + .pipe(gulp.dest('_output/UI')) .on('error', errorHandler) .pipe(livereload()) .on('error', errorHandler); diff --git a/frontend/src/Activity/Blacklist/BlacklistRow.js b/frontend/src/Activity/Blacklist/BlacklistRow.js index 2ec805678..22e43f9c3 100644 --- a/frontend/src/Activity/Blacklist/BlacklistRow.js +++ b/frontend/src/Activity/Blacklist/BlacklistRow.js @@ -53,6 +53,10 @@ class BlacklistRow extends Component { onRemovePress } = this.props; + if (!movie) { + return null; + } + return ( { diff --git a/frontend/src/Activity/History/History.js b/frontend/src/Activity/History/History.js index e5ab7f830..123cb1e30 100644 --- a/frontend/src/Activity/History/History.js +++ b/frontend/src/Activity/History/History.js @@ -24,6 +24,9 @@ class History extends Component { isFetching, isPopulated, error, + isMoviesFetching, + isMoviesPopulated, + moviesError, items, columns, selectedFilterKey, @@ -34,7 +37,9 @@ class History extends Component { ...otherProps } = this.props; - const hasError = error; + const isFetchingAny = isFetching || isMoviesFetching; + const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length); + const hasError = error || moviesError; return ( @@ -71,12 +76,12 @@ class History extends Component { { - isFetching && !isPopulated && + isFetchingAny && !isAllPopulated && } { - !isFetching && hasError && + !isFetchingAny && hasError &&
Unable to load history
} @@ -91,7 +96,7 @@ class History extends Component { } { - isPopulated && !hasError && !!items.length && + isAllPopulated && !hasError && !!items.length &&
state.history, - (history) => { + (state) => state.movies, + (history, movies) => { return { + isMoviesFetching: movies.isFetching, + isMoviesPopulated: movies.isPopulated, + moviesError: movies.error, ...history }; } diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js index 0ddcd2e8c..709c407ac 100644 --- a/frontend/src/Activity/Queue/Queue.js +++ b/frontend/src/Activity/Queue/Queue.js @@ -104,6 +104,9 @@ class Queue extends Component { isFetching, isPopulated, error, + isMoviesFetching, + isMoviesPopulated, + moviesError, items, columns, totalRecords, @@ -122,8 +125,9 @@ class Queue extends Component { isPendingSelected } = this.state; - const isRefreshing = isFetching || isCheckForFinishedDownloadExecuting; - const hasError = error; + const isRefreshing = isFetching || isMoviesFetching || isCheckForFinishedDownloadExecuting; + const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId)); + const hasError = error || moviesError; const selectedCount = this.getSelectedIds().length; const disableSelectedActions = selectedCount === 0; @@ -175,7 +179,7 @@ class Queue extends Component { { - isRefreshing && !isPopulated && + isRefreshing && !isAllPopulated && } @@ -194,7 +198,7 @@ class Queue extends Component { } { - isPopulated && !hasError && !!items.length && + isAllPopulated && !hasError && !!items.length &&
state.movies, (state) => state.queue.options, (state) => state.queue.paged, createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD), - (options, queue, isCheckForFinishedDownloadExecuting) => { + (movies, options, queue, isCheckForFinishedDownloadExecuting) => { return { + isMoviesFetching: movies.isFetching, + isMoviesPopulated: movies.isPopulated, + moviesError: movies.error, isCheckForFinishedDownloadExecuting, ...options, ...queue diff --git a/frontend/src/AddMovie/AddListMovie/AddDiscoverMovieConnector.js b/frontend/src/AddMovie/AddListMovie/AddDiscoverMovieConnector.js index 819b459ef..60d6e536e 100644 --- a/frontend/src/AddMovie/AddListMovie/AddDiscoverMovieConnector.js +++ b/frontend/src/AddMovie/AddListMovie/AddDiscoverMovieConnector.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import dimensions from 'Styles/Variables/dimensions'; import createAddMovieClientSideCollectionItemsSelector from 'Store/Selectors/createAddMovieClientSideCollectionItemsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; @@ -12,29 +11,6 @@ import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePo import withScrollPosition from 'Components/withScrollPosition'; import AddListMovie from './AddListMovie'; -const POSTERS_PADDING = 15; -const POSTERS_PADDING_SMALL_SCREEN = 5; -const TABLE_PADDING = parseInt(dimensions.pageContentBodyPadding); -const TABLE_PADDING_SMALL_SCREEN = parseInt(dimensions.pageContentBodyPaddingSmallScreen); - -// If the scrollTop is greater than zero it needs to be offset -// by the padding so when it is set initially so it is correct -// after React Virtualized takes the padding into account. - -function getScrollTop(view, scrollTop, isSmallScreen) { - if (scrollTop === 0) { - return 0; - } - - let padding = isSmallScreen ? TABLE_PADDING_SMALL_SCREEN : TABLE_PADDING; - - if (view === 'posters') { - padding = isSmallScreen ? POSTERS_PADDING_SMALL_SCREEN : POSTERS_PADDING; - } - - return scrollTop + padding; -} - function createMapStateToProps() { return createSelector( createAddMovieClientSideCollectionItemsSelector('addMovie'), @@ -88,20 +64,6 @@ class AddDiscoverMovieConnector extends Component { // // Lifecycle - constructor(props, context) { - super(props, context); - - const { - view, - scrollTop, - isSmallScreen - } = props; - - this.state = { - scrollTop: getScrollTop(view, scrollTop, isSmallScreen) - }; - } - componentDidMount() { registerPagePopulator(this.repopulate); this.props.dispatchFetchRootFolders(); @@ -117,18 +79,11 @@ class AddDiscoverMovieConnector extends Component { // Listeners onViewSelect = (view) => { - // Reset the scroll position before changing the view - this.setState({ scrollTop: 0 }, () => { - this.props.dispatchSetListMovieView(view); - }); + this.props.dispatchSetListMovieView(view); } onScroll = ({ scrollTop }) => { - this.setState({ - scrollTop - }, () => { - scrollPositions.addMovie = scrollTop; - }); + scrollPositions.addMovie = scrollTop; } // @@ -138,7 +93,6 @@ class AddDiscoverMovieConnector extends Component { return ( { - this.setState({ contentBody: ref }); + setScrollerRef = (ref) => { + this.setState({ scroller: ref }); } setJumpBarItems() { @@ -96,29 +93,39 @@ class AddListMovie extends Component { // Reset if not sorting by sortTitle if (sortKey !== 'sortTitle') { - this.setState({ jumpBarItems: [] }); + this.setState({ jumpBarItems: { order: [] } }); return; } const characters = _.reduce(items, (acc, item) => { + let char = item.sortTitle.charAt(0); - const firstCharacter = item.sortTitle.charAt(0); + if (!isNaN(char)) { + char = '#'; + } - if (isNaN(firstCharacter)) { - acc.push(firstCharacter); + if (char in acc) { + acc[char] = acc[char] + 1; } else { - acc.push('#'); + acc[char] = 1; } return acc; - }, []).sort(); + }, {}); + + const order = Object.keys(characters).sort(); // Reverse if sorting descending if (sortDirection === sortDirections.DESCENDING) { - characters.reverse(); + order.reverse(); } - this.setState({ jumpBarItems: _.sortedUniq(characters) }); + const jumpBarItems = { + characters, + order + }; + + this.setState({ jumpBarItems }); } // @@ -144,27 +151,6 @@ class AddListMovie extends Component { this.setState({ jumpToCharacter }); } - onRender = () => { - this.setState({ isRendered: true }, () => { - const { - scrollTop, - isSmallScreen - } = this.props; - - if (isSmallScreen) { - // Seems to result in the view being off by 125px (distance to the top of the page) - // document.documentElement.scrollTop = document.body.scrollTop = scrollTop; - - // This works, but then jumps another 1px after scrolling - document.documentElement.scrollTop = scrollTop; - } - }); - } - - onScroll = ({ scrollTop }) => { - this.props.onScroll({ scrollTop }); - } - // // Render @@ -182,24 +168,23 @@ class AddListMovie extends Component { sortKey, sortDirection, view, - scrollTop, onSortSelect, onFilterSelect, onViewSelect, + onScroll, ...otherProps } = this.props; const { - contentBody, + scroller, jumpBarItems, jumpToCharacter, isPosterOptionsModalOpen, - isOverviewOptionsModalOpen, - isRendered + isOverviewOptionsModalOpen } = this.state; const ViewComponent = getViewComponent(view); - const isLoaded = !!(!error && isPopulated && items.length && contentBody); + const isLoaded = !!(!error && isPopulated && items.length && scroller); const hasNoMovie = !totalItems; return ( @@ -275,11 +260,10 @@ class AddListMovie extends Component {
{ isFetching && !isPopulated && @@ -295,14 +279,12 @@ class AddListMovie extends Component { isLoaded &&
@@ -317,7 +299,7 @@ class AddListMovie extends Component {
{ - isLoaded && !!jumpBarItems.length && + isLoaded && !!jumpBarItems.order.length && { - // Reset the scroll position before changing the view - this.setState({ scrollTop: 0 }, () => { - this.props.dispatchSetListMovieView(view); - }); + this.props.dispatchSetListMovieView(view); } onScroll = ({ scrollTop }) => { - this.setState({ - scrollTop - }, () => { - scrollPositions.addMovie = scrollTop; - }); + scrollPositions.addMovie = scrollTop; } // @@ -138,7 +90,6 @@ class AddListMovieConnector extends Component { return ( +
{ this._grid = ref; } @@ -179,22 +149,27 @@ class AddListMovieOverviews extends Component { } return ( - + > + +
); } @@ -205,22 +180,14 @@ class AddListMovieOverviews extends Component { this.calculateGrid(width, this.props.isSmallScreen); } - onSectionRendered = () => { - if (!this._isInitialized && this._contentBodyNode) { - this.props.onRender(); - this._isInitialized = true; - } - } - // // Render render() { const { - items, - scrollTop, isSmallScreen, - onScroll + scroller, + items } = this.props; const { @@ -229,28 +196,34 @@ class AddListMovieOverviews extends Component { } = this.state; return ( - + - {({ height, isScrolling }) => { + {({ height, registerChild, onChildScroll, scrollTop }) => { return ( - +
+ +
); } } @@ -262,20 +235,15 @@ class AddListMovieOverviews extends Component { AddListMovieOverviews.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, - sortDirection: PropTypes.oneOf(sortDirections.all), overviewOptions: PropTypes.object.isRequired, - scrollTop: PropTypes.number.isRequired, jumpToCharacter: PropTypes.string, - contentBody: PropTypes.object.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, showRelativeDates: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired, isSmallScreen: PropTypes.bool.isRequired, - timeFormat: PropTypes.string.isRequired, - onRender: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired + timeFormat: PropTypes.string.isRequired }; export default AddListMovieOverviews; diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.js b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.js index 72fec309f..f333ab821 100644 --- a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.js +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePoster.js @@ -47,7 +47,6 @@ class AddListMoviePoster extends Component { render() { const { - style, tmdbId, title, year, @@ -75,67 +74,64 @@ class AddListMoviePoster extends Component { }; return ( -
-
-
- { - status === 'ended' && -
- } - - - - - { - hasPosterError && -
- {title} -
- } - -
- +
+
{ - showTitle && -
- {title} -
+ status === 'ended' && +
} - + + + + { + hasPosterError && +
+ {title} +
+ } +
+ + { + showTitle && +
+ {title} +
+ } + +
); } } AddListMoviePoster.propTypes = { - style: PropTypes.object.isRequired, tmdbId: PropTypes.number.isRequired, title: PropTypes.string.isRequired, year: PropTypes.number.isRequired, diff --git a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.js b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.js index b123330ae..e28ba812c 100644 --- a/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.js +++ b/frontend/src/AddMovie/AddListMovie/Posters/AddListMoviePosters.js @@ -1,11 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; import { Grid, WindowScroller } from 'react-virtualized'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItems'; import dimensions from 'Styles/Variables/dimensions'; -import { sortDirections } from 'Helpers/Props'; import Measure from 'Components/Measure'; import AddListMovieItemConnector from 'AddMovie/AddListMovie/AddListMovieItemConnector'; import AddListMoviePosterConnector from './AddListMoviePosterConnector'; @@ -98,52 +96,46 @@ class AddListMoviePosters extends Component { this._grid = null; } - componentDidMount() { - this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); - } - - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps, prevState) { const { items, - filters, sortKey, - sortDirection, posterOptions, jumpToCharacter } = this.props; - const itemsChanged = hasDifferentItems(prevProps.items, items); + const { + width, + columnWidth, + columnCount, + rowHeight + } = this.state; - if ( - prevProps.sortKey !== sortKey || - prevProps.posterOptions !== posterOptions || - itemsChanged - ) { + if (prevProps.sortKey !== sortKey || + prevProps.posterOptions !== posterOptions) { this.calculateGrid(); } - if ( - prevProps.filters !== filters || - prevProps.sortKey !== sortKey || - prevProps.sortDirection !== sortDirection || - itemsChanged - ) { + if (this._grid && + (prevState.width !== width || + prevState.columnWidth !== columnWidth || + prevState.columnCount !== columnCount || + prevState.rowHeight !== rowHeight || + hasDifferentItemsOrOrder(prevProps.items, items))) { + // recomputeGridSize also forces Grid to discard its cache of rendered cells this._grid.recomputeGridSize(); } if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { const index = getIndexOfFirstCharacter(items, jumpToCharacter); - if (index != null) { - const { - columnCount, - rowHeight - } = this.state; - + if (this._grid && index != null) { const row = Math.floor(index / columnCount); - const scrollTop = rowHeight * row; - this.props.onScroll({ scrollTop }); + this._grid.scrollToCell({ + rowIndex: row, + columnIndex: 0 + }); } } } @@ -205,19 +197,23 @@ class AddListMoviePosters extends Component { } return ( - + > + +
); } @@ -228,22 +224,14 @@ class AddListMoviePosters extends Component { this.calculateGrid(width, this.props.isSmallScreen); } - onSectionRendered = () => { - if (!this._isInitialized && this._contentBodyNode) { - this.props.onRender(); - this._isInitialized = true; - } - } - // // Render render() { const { - items, - scrollTop, isSmallScreen, - onScroll + scroller, + items } = this.props; const { @@ -256,28 +244,34 @@ class AddListMoviePosters extends Component { const rowCount = Math.ceil(items.length / columnCount); return ( - + - {({ height, isScrolling }) => { + {({ height, registerChild, onChildScroll, scrollTop }) => { return ( - +
+ +
); } } @@ -289,19 +283,14 @@ class AddListMoviePosters extends Component { AddListMoviePosters.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, - sortDirection: PropTypes.oneOf(sortDirections.all), posterOptions: PropTypes.object.isRequired, - scrollTop: PropTypes.number.isRequired, jumpToCharacter: PropTypes.string, - contentBody: PropTypes.object.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, showRelativeDates: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, isSmallScreen: PropTypes.bool.isRequired, - timeFormat: PropTypes.string.isRequired, - onRender: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired + timeFormat: PropTypes.string.isRequired }; export default AddListMoviePosters; diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.js b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.js index b789b7c7c..2a96225cc 100644 --- a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.js +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieRow.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import HeartRating from 'Components/HeartRating'; -import VirtualTableRow from 'Components/Table/VirtualTableRow'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import MovieStatusCell from './MovieStatusCell'; @@ -38,7 +37,6 @@ class AddListMovieRow extends Component { render() { const { - style, status, tmdbId, title, @@ -64,140 +62,136 @@ class AddListMovieRow extends Component { const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress }; return ( -
- - { - columns.map((column) => { - const { - name, - isVisible - } = column; - - if (!isVisible) { - return null; - } - - if (name === 'status') { - return ( - - ); - } - - if (name === 'sortTitle') { - return ( - - - {title} - - - ); - } - - if (name === 'studio') { - return ( - + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'status') { + return ( + + ); + } + + if (name === 'sortTitle') { + return ( + + - {studio} - - ); - } - - if (name === 'inCinemas') { - return ( - + + ); + } + + if (name === 'studio') { + return ( + + {studio} + + ); + } + + if (name === 'inCinemas') { + return ( + + ); + } + + if (name === 'physicalRelease') { + return ( + + ); + } + + if (name === 'genres') { + const joinedGenres = genres.join(', '); + + return ( + + + {joinedGenres} + + + ); + } + + if (name === 'ratings') { + return ( + + - ); - } - - if (name === 'physicalRelease') { - return ( - - ); - } - - if (name === 'genres') { - const joinedGenres = genres.join(', '); - - return ( - - - {joinedGenres} - - - ); - } - - if (name === 'ratings') { - return ( - - - - ); - } - - if (name === 'certification') { - return ( - - {certification} - - ); - } - - return null; - }) - } - - - -
+ + ); + } + + if (name === 'certification') { + return ( + + {certification} + + ); + } + + return null; + }) + } + + + ); } } AddListMovieRow.propTypes = { - style: PropTypes.object.isRequired, tmdbId: PropTypes.number.isRequired, status: PropTypes.string.isRequired, title: PropTypes.string.isRequired, diff --git a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.js b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.js index 535c3bb07..8d8faf42e 100644 --- a/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.js +++ b/frontend/src/AddMovie/AddListMovie/Table/AddListMovieTable.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; import { sortDirections } from 'Helpers/Props'; import VirtualTable from 'Components/Table/VirtualTable'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; import AddListMovieItemConnector from 'AddMovie/AddListMovie/AddListMovieItemConnector'; import AddListMovieHeaderConnector from './AddListMovieHeaderConnector'; import AddListMovieRowConnector from './AddListMovieRowConnector'; @@ -23,11 +24,10 @@ class AddListMovieTable extends Component { componentDidUpdate(prevProps) { const { - items + items, + jumpToCharacter } = this.props; - const jumpToCharacter = this.props.jumpToCharacter; - if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter); @@ -52,13 +52,17 @@ class AddListMovieTable extends Component { const movie = items[rowIndex]; return ( - + > + + ); } @@ -69,25 +73,18 @@ class AddListMovieTable extends Component { const { items, columns, - filters, sortKey, sortDirection, - isSmallScreen, - scrollTop, - contentBody, - onSortPress, - onRender, - onScroll + scroller, + onSortPress } = this.props; return ( } columns={columns} - filters={filters} - sortKey={sortKey} - sortDirection={sortDirection} - onRender={onRender} - onScroll={onScroll} /> ); } @@ -113,16 +105,11 @@ class AddListMovieTable extends Component { AddListMovieTable.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, sortDirection: PropTypes.oneOf(sortDirections.all), - scrollTop: PropTypes.number.isRequired, jumpToCharacter: PropTypes.string, - contentBody: PropTypes.object.isRequired, - isSmallScreen: PropTypes.bool.isRequired, - onSortPress: PropTypes.func.isRequired, - onRender: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired + scroller: PropTypes.instanceOf(Element).isRequired, + onSortPress: PropTypes.func.isRequired }; export default AddListMovieTable; diff --git a/frontend/src/AddMovie/ImportMovie/Import/ImportMovie.js b/frontend/src/AddMovie/ImportMovie/Import/ImportMovie.js index a649f9f90..0893172e3 100644 --- a/frontend/src/AddMovie/ImportMovie/Import/ImportMovie.js +++ b/frontend/src/AddMovie/ImportMovie/Import/ImportMovie.js @@ -22,16 +22,15 @@ class ImportMovie extends Component { allUnselected: false, lastToggled: null, selectedState: {}, - contentBody: null, - scrollTop: 0 + contentBody: null }; } // // Control - setContentBodyRef = (ref) => { - this.setState({ contentBody: ref }); + setScrollerRef = (ref) => { + this.setState({ scroller: ref }); } // @@ -72,10 +71,6 @@ class ImportMovie extends Component { this.props.onImportPress(this.getSelectedIds()); } - onScroll = ({ scrollTop }) => { - this.setState({ scrollTop }); - } - // // Render @@ -93,13 +88,13 @@ class ImportMovie extends Component { allSelected, allUnselected, selectedState, - contentBody + scroller } = this.state; return ( { @@ -120,19 +115,17 @@ class ImportMovie extends Component { } { - !rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody && + !rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller && } diff --git a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieRow.js b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieRow.js index 7802b901f..7b32e3d7a 100644 --- a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieRow.js +++ b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieRow.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import { inputTypes } from 'Helpers/Props'; import FormInputGroup from 'Components/Form/FormInputGroup'; -import VirtualTableRow from 'Components/Table/VirtualTableRow'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; import ImportMovieSelectMovieConnector from './SelectMovie/ImportMovieSelectMovieConnector'; @@ -10,7 +9,6 @@ import styles from './ImportMovieRow.css'; function ImportMovieRow(props) { const { - style, id, monitor, qualityProfileId, @@ -23,7 +21,7 @@ function ImportMovieRow(props) { } = props; return ( - + <> - + ); } ImportMovieRow.propTypes = { - style: PropTypes.object.isRequired, id: PropTypes.string.isRequired, monitor: PropTypes.string.isRequired, qualityProfileId: PropTypes.number.isRequired, diff --git a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTable.js b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTable.js index e048eea80..89665fb0a 100644 --- a/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTable.js +++ b/frontend/src/AddMovie/ImportMovie/Import/ImportMovieTable.js @@ -2,6 +2,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import VirtualTable from 'Components/Table/VirtualTable'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; import ImportMovieHeader from './ImportMovieHeader'; import ImportMovieRowConnector from './ImportMovieRowConnector'; @@ -107,14 +108,18 @@ class ImportMovieTable extends Component { const item = items[rowIndex]; return ( - + > + + ); } @@ -127,11 +132,9 @@ class ImportMovieTable extends Component { allSelected, allUnselected, isSmallScreen, - contentBody, - scrollTop, + scroller, selectedState, - onSelectAllChange, - onScroll + onSelectAllChange } = this.props; if (!items.length) { @@ -141,10 +144,9 @@ class ImportMovieTable extends Component { return ( } selectedState={selectedState} - onScroll={onScroll} /> ); } @@ -173,14 +174,12 @@ ImportMovieTable.propTypes = { selectedState: PropTypes.object.isRequired, isSmallScreen: PropTypes.bool.isRequired, allMovies: PropTypes.arrayOf(PropTypes.object), - contentBody: PropTypes.object.isRequired, - scrollTop: PropTypes.number.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, onSelectAllChange: PropTypes.func.isRequired, onSelectedChange: PropTypes.func.isRequired, onRemoveSelectedStateItem: PropTypes.func.isRequired, onMovieLookup: PropTypes.func.isRequired, - onSetImportMovieValue: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired + onSetImportMovieValue: PropTypes.func.isRequired }; export default ImportMovieTable; diff --git a/frontend/src/AddMovie/ImportMovie/Import/SelectMovie/ImportMovieSelectMovie.css b/frontend/src/AddMovie/ImportMovie/Import/SelectMovie/ImportMovieSelectMovie.css index 8eb523430..2b9cef136 100644 --- a/frontend/src/AddMovie/ImportMovie/Import/SelectMovie/ImportMovieSelectMovie.css +++ b/frontend/src/AddMovie/ImportMovie/Import/SelectMovie/ImportMovieSelectMovie.css @@ -3,6 +3,7 @@ display: flex; align-items: center; + justify-content: space-between; padding: 6px 16px; width: 100%; height: 35px; @@ -25,7 +26,6 @@ } .dropdownArrowContainer { - position: absolute; right: 16px; } diff --git a/frontend/src/Calendar/CalendarPage.css b/frontend/src/Calendar/CalendarPage.css index 90ba5c505..b6839c467 100644 --- a/frontend/src/Calendar/CalendarPage.css +++ b/frontend/src/Calendar/CalendarPage.css @@ -12,3 +12,9 @@ flex-grow: 1; width: 100%; } + +.errorMessage { + margin-top: 20px; + text-align: center; + font-size: 20px; +} diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js index 3f93fcad8..c70182c98 100644 --- a/frontend/src/Calendar/CalendarPage.js +++ b/frontend/src/Calendar/CalendarPage.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; import { align, icons } from 'Helpers/Props'; import PageContent from 'Components/Page/PageContent'; import Measure from 'Components/Measure'; @@ -75,6 +76,7 @@ class CalendarPage extends Component { selectedFilterKey, filters, hasMovie, + movieError, missingMovieIds, isSearchingForMissing, useCurrentPage, @@ -130,21 +132,31 @@ class CalendarPage extends Component { className={styles.calendarPageBody} innerClassName={styles.calendarInnerPageBody} > - - { - isMeasured ? - : -
- } - + { + movieError && +
+ {getErrorMessage(movieError, 'Failed to load movie from API')} +
+ } + + { + !movieError && + + { + isMeasured ? + : +
+ } + + } { - hasMovie && + hasMovie && !! movieError && } @@ -167,6 +179,7 @@ CalendarPage.propTypes = { selectedFilterKey: PropTypes.string.isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired, hasMovie: PropTypes.bool.isRequired, + movieError: PropTypes.object, missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired, isSearchingForMissing: PropTypes.bool.isRequired, useCurrentPage: PropTypes.bool.isRequired, diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js index 7131db7a5..c8446940a 100644 --- a/frontend/src/Calendar/CalendarPageConnector.js +++ b/frontend/src/Calendar/CalendarPageConnector.js @@ -72,7 +72,8 @@ function createMapStateToProps() { selectedFilterKey, filters, colorImpairedMode: uiSettings.enableColorImpairedMode, - hasMovie: !!movieCount, + hasMovie: !!movieCount.count, + movieError: movieCount.error, missingMovieIds, isSearchingForMissing }; diff --git a/frontend/src/Components/Page/Header/MovieSearchInput.js b/frontend/src/Components/Page/Header/MovieSearchInput.js index 199a9a1b1..2607a49a1 100644 --- a/frontend/src/Components/Page/Header/MovieSearchInput.js +++ b/frontend/src/Components/Page/Header/MovieSearchInput.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 MovieSearchResult from './MovieSearchResult'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FuseWorker from './fuse.worker'; import styles from './MovieSearchInput.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 MovieSearchInput extends Component { @@ -43,6 +32,7 @@ class MovieSearchInput extends Component { componentDidMount() { this.props.bindShortcut(shortcuts.MOVIE_SEARCH_INPUT.key, this.focusInput); + workerInstance.addEventListener('message', this.onSuggestionsReceived, false); } // @@ -82,6 +72,12 @@ class MovieSearchInput extends Component { ); } + if (item.type === LOADING_TYPE) { + return ( + + ); + } + return ( { - const { movies } = this.props; - let suggestions = []; - - if (value.length === 1) { - suggestions = movies.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(movies, fuseOptions); - suggestions = fuse.search(value); - } + requestSuggestions = _.debounce((value) => { + const payload = { + value, + movies: this.props.movies + }; + + 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..52f4e52c6 --- /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(movies, value) { + const limit = 10; + let suggestions = []; + + if (value.length === 1) { + for (let i = 0; i < movies.length; i++) { + const s = movies[i]; + if (s.firstCharacter === value.toLowerCase()) { + suggestions.push({ + item: movies[i], + indices: [ + [0, 0] + ], + matches: [ + { + value: s.title, + key: 'title' + } + ], + arrayIndex: 0 + }); + if (suggestions.length > limit) { + break; + } + } + } + } else { + const fuse = new Fuse(movies, fuseOptions); + suggestions = fuse.search(value, { limit }); + } + + return suggestions; +} + +self.addEventListener('message', (e) => { + if (!e) { + return; + } + + const { + movies, + value + } = e.data; + + self.postMessage(getSuggestions(movies, value)); +}); diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js index 516548b75..664a48d3c 100644 --- a/frontend/src/Components/Page/PageConnector.js +++ b/frontend/src/Components/Page/PageConnector.js @@ -43,7 +43,6 @@ const selectAppProps = createSelector( ); const selectIsPopulated = createSelector( - (state) => state.movies.isPopulated, (state) => state.customFilters.isPopulated, (state) => state.tags.isPopulated, (state) => state.settings.ui.isPopulated, @@ -51,7 +50,6 @@ const selectIsPopulated = createSelector( (state) => state.settings.languages.isPopulated, (state) => state.system.status.isPopulated, ( - moviesIsPopulated, customFiltersIsPopulated, tagsIsPopulated, uiSettingsIsPopulated, @@ -60,7 +58,6 @@ const selectIsPopulated = createSelector( systemStatusIsPopulated ) => { return ( - moviesIsPopulated && customFiltersIsPopulated && tagsIsPopulated && uiSettingsIsPopulated && @@ -72,7 +69,6 @@ const selectIsPopulated = createSelector( ); const selectErrors = createSelector( - (state) => state.movies.error, (state) => state.customFilters.error, (state) => state.tags.error, (state) => state.settings.ui.error, @@ -80,7 +76,6 @@ const selectErrors = createSelector( (state) => state.settings.languages.error, (state) => state.system.status.error, ( - moviesError, customFiltersError, tagsError, uiSettingsError, @@ -89,7 +84,6 @@ const selectErrors = createSelector( systemStatusError ) => { const hasError = !!( - moviesError || customFiltersError || tagsError || uiSettingsError || @@ -100,7 +94,6 @@ const selectErrors = createSelector( return { hasError, - moviesError, customFiltersError, tagsError, uiSettingsError, diff --git a/frontend/src/Components/Page/PageJumpBar.js b/frontend/src/Components/Page/PageJumpBar.js index 41df52dfc..4b73ad4cb 100644 --- a/frontend/src/Components/Page/PageJumpBar.js +++ b/frontend/src/Components/Page/PageJumpBar.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import dimensions from 'Styles/Variables/dimensions'; @@ -18,7 +17,7 @@ class PageJumpBar extends Component { this.state = { height: 0, - visibleItems: props.items + visibleItems: props.items.order }; } @@ -52,29 +51,47 @@ class PageJumpBar extends Component { minimumItems } = this.props; + if (!items) { + return; + } + + const { + characters, + order + } = items; + const height = this.state.height; const maximumItems = Math.floor(height / ITEM_HEIGHT); - const diff = items.length - maximumItems; + const diff = order.length - maximumItems; if (diff < 0) { - this.setState({ visibleItems: items }); + this.setState({ visibleItems: order }); return; } - if (items.length < minimumItems) { - this.setState({ visibleItems: items }); + if (order.length < minimumItems) { + this.setState({ visibleItems: order }); return; } - const removeDiff = Math.ceil(items.length / maximumItems); + // get first, last, and most common in between to make up numbers + const visibleItems = [order[0]]; + + const sorted = order.slice(1, -1).map((x) => characters[x]).sort((a, b) => b - a); + const minCount = sorted[maximumItems - 3]; + const greater = sorted.reduce((acc, value) => acc + (value > minCount ? 1 : 0), 0); + let minAllowed = maximumItems - 2 - greater; - const visibleItems = _.reduce(items, (acc, item, index) => { - if (index % removeDiff === 0) { - acc.push(item); + for (let i = 1; i < order.length - 1; i++) { + if (characters[order[i]] > minCount) { + visibleItems.push(order[i]); + } else if (characters[order[i]] === minCount && minAllowed > 0) { + visibleItems.push(order[i]); + minAllowed--; } + } - return acc; - }, []); + visibleItems.push(order[order.length - 1]); this.setState({ visibleItems }); } @@ -129,7 +146,7 @@ class PageJumpBar extends Component { } PageJumpBar.propTypes = { - items: PropTypes.arrayOf(PropTypes.string).isRequired, + items: PropTypes.object.isRequired, minimumItems: PropTypes.number.isRequired, onItemPress: PropTypes.func.isRequired }; diff --git a/frontend/src/Components/Page/PageJumpBarItem.css b/frontend/src/Components/Page/PageJumpBarItem.css index e829dd31a..f1a0c3699 100644 --- a/frontend/src/Components/Page/PageJumpBarItem.css +++ b/frontend/src/Components/Page/PageJumpBarItem.css @@ -1,5 +1,5 @@ .jumpBarItem { - flex: 1 0 $jumpBarItemHeight; + flex: 1 1 $jumpBarItemHeight; border-bottom: 1px solid $borderColor; text-align: center; font-weight: bold; diff --git a/frontend/src/Components/Scroller/OverlayScroller.js b/frontend/src/Components/Scroller/OverlayScroller.js index e2a269bdc..bb8120ff8 100644 --- a/frontend/src/Components/Scroller/OverlayScroller.js +++ b/frontend/src/Components/Scroller/OverlayScroller.js @@ -37,6 +37,10 @@ class OverlayScroller extends Component { _setScrollRef = (ref) => { this._scroller = ref; + + if (ref) { + this.props.registerScroller(ref.view); + } } _renderThumb = (props) => { @@ -157,7 +161,8 @@ OverlayScroller.propTypes = { autoHide: PropTypes.bool.isRequired, autoScroll: PropTypes.bool.isRequired, children: PropTypes.node, - onScroll: PropTypes.func + onScroll: PropTypes.func, + registerScroller: PropTypes.func }; OverlayScroller.defaultProps = { @@ -165,7 +170,8 @@ OverlayScroller.defaultProps = { trackClassName: styles.thumb, scrollDirection: scrollDirections.VERTICAL, autoHide: false, - autoScroll: true + autoScroll: true, + registerScroller: () => {} }; export default OverlayScroller; diff --git a/frontend/src/Components/Scroller/Scroller.js b/frontend/src/Components/Scroller/Scroller.js index f4ce7781f..22fae04c6 100644 --- a/frontend/src/Components/Scroller/Scroller.js +++ b/frontend/src/Components/Scroller/Scroller.js @@ -30,6 +30,8 @@ class Scroller extends Component { _setScrollerRef = (ref) => { this._scroller = ref; + + this.props.registerScroller(ref); } // @@ -43,6 +45,7 @@ class Scroller extends Component { children, scrollTop, onScroll, + registerScroller, ...otherProps } = this.props; @@ -70,12 +73,14 @@ Scroller.propTypes = { autoScroll: PropTypes.bool.isRequired, scrollTop: PropTypes.number, children: PropTypes.node, - onScroll: PropTypes.func + onScroll: PropTypes.func, + registerScroller: PropTypes.func }; Scroller.defaultProps = { scrollDirection: scrollDirections.VERTICAL, - autoScroll: true + autoScroll: true, + registerScroller: () => {} }; export default Scroller; diff --git a/frontend/src/Components/Table/VirtualTable.css b/frontend/src/Components/Table/VirtualTable.css index 3287c5643..81111bcf9 100644 --- a/frontend/src/Components/Table/VirtualTable.css +++ b/frontend/src/Components/Table/VirtualTable.css @@ -1,3 +1,7 @@ .tableContainer { width: 100%; } + +.tableBodyContainer { + position: relative; +} diff --git a/frontend/src/Components/Table/VirtualTable.js b/frontend/src/Components/Table/VirtualTable.js index 258d31b00..18b2bbb78 100644 --- a/frontend/src/Components/Table/VirtualTable.js +++ b/frontend/src/Components/Table/VirtualTable.js @@ -1,12 +1,10 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; -import { WindowScroller } from 'react-virtualized'; -import { isLocked } from 'Utilities/scrollLock'; import { scrollDirections } from 'Helpers/Props'; import Measure from 'Components/Measure'; import Scroller from 'Components/Scroller/Scroller'; -import VirtualTableBody from './VirtualTableBody'; +import { WindowScroller, Grid } from 'react-virtualized'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import styles from './VirtualTable.css'; const ROW_HEIGHT = 38; @@ -44,28 +42,39 @@ class VirtualTable extends Component { width: 0 }; - this._isInitialized = false; + this._grid = null; } - componentDidMount() { - this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); - } + componentDidUpdate(prevProps, prevState) { + const { + items, + scrollIndex + } = this.props; - componentDidUpdate(prevProps, preState) { - const scrollIndex = this.props.scrollIndex; + const { + width + } = this.state; - if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) { - const scrollTop = (scrollIndex + 1) * ROW_HEIGHT + 20; + if (this._grid && + (prevState.width !== width || + hasDifferentItemsOrOrder(prevProps.items, items))) { + // recomputeGridSize also forces Grid to discard its cache of rendered cells + this._grid.recomputeGridSize(); + } - this.props.onScroll({ scrollTop }); + if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) { + this._grid.scrollToCell({ + rowIndex: scrollIndex, + columnIndex: 0 + }); } } // // Control - rowGetter = ({ index }) => { - return this.props.items[index]; + setGridRef = (ref) => { + this._grid = ref; } // @@ -77,36 +86,18 @@ class VirtualTable extends Component { }); } - onSectionRendered = () => { - if (!this._isInitialized && this._contentBodyNode) { - this.props.onRender(); - this._isInitialized = true; - } - } - - onScroll = (props) => { - if (isLocked()) { - return; - } - - const { onScroll } = this.props; - - onScroll(props); - } - // // Render render() { const { + isSmallScreen, className, items, - isSmallScreen, + scroller, header, headerHeight, - scrollTop, rowRenderer, - onScroll, ...otherProps } = this.props; @@ -114,65 +105,88 @@ class VirtualTable extends Component { width } = this.state; + const gridStyle = { + boxSizing: undefined, + direction: undefined, + height: undefined, + position: undefined, + willChange: undefined, + overflow: undefined, + width: undefined + }; + + const containerStyle = { + position: undefined + }; + return ( - - - {({ height, isScrolling }) => { - return ( + + {({ height, registerChild, onChildScroll, scrollTop }) => { + if (!height) { + return null; + } + return ( + {header} - - +
+ +
- ); - } - } -
-
+ + ); + } + } + ); } } VirtualTable.propTypes = { + isSmallScreen: PropTypes.bool.isRequired, className: PropTypes.string.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, - scrollTop: PropTypes.number.isRequired, scrollIndex: PropTypes.number, - contentBody: PropTypes.object.isRequired, - isSmallScreen: PropTypes.bool.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, header: PropTypes.node.isRequired, headerHeight: PropTypes.number.isRequired, - rowRenderer: PropTypes.func.isRequired, - onRender: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired + rowRenderer: PropTypes.func.isRequired }; VirtualTable.defaultProps = { className: styles.tableContainer, - headerHeight: 38, - onRender: () => {} + headerHeight: 38 }; export default VirtualTable; diff --git a/frontend/src/Components/Table/VirtualTableBody.css b/frontend/src/Components/Table/VirtualTableBody.css deleted file mode 100644 index 12768646d..000000000 --- a/frontend/src/Components/Table/VirtualTableBody.css +++ /dev/null @@ -1,3 +0,0 @@ -.tableBodyContainer { - position: relative; -} diff --git a/frontend/src/Components/Table/VirtualTableBody.js b/frontend/src/Components/Table/VirtualTableBody.js deleted file mode 100644 index de88bd03c..000000000 --- a/frontend/src/Components/Table/VirtualTableBody.js +++ /dev/null @@ -1,40 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Grid } from 'react-virtualized'; -import styles from './VirtualTableBody.css'; - -class VirtualTableBody extends Component { - - // - // Render - - render() { - return ( - - ); - } -} - -VirtualTableBody.propTypes = { - className: PropTypes.string.isRequired -}; - -VirtualTableBody.defaultProps = { - className: styles.tableBodyContainer -}; - -export default VirtualTableBody; diff --git a/frontend/src/Movie/Details/MovieDetails.css b/frontend/src/Movie/Details/MovieDetails.css index 9e100c0f8..2051f8326 100644 --- a/frontend/src/Movie/Details/MovieDetails.css +++ b/frontend/src/Movie/Details/MovieDetails.css @@ -8,6 +8,12 @@ height: 350px; } +.errorMessage { + margin-top: 20px; + text-align: center; + font-size: 20px; +} + .backdrop { position: absolute; z-index: -1; diff --git a/frontend/src/Movie/Details/MovieDetailsPageConnector.js b/frontend/src/Movie/Details/MovieDetailsPageConnector.js index b8e873b64..97250ab88 100644 --- a/frontend/src/Movie/Details/MovieDetailsPageConnector.js +++ b/frontend/src/Movie/Details/MovieDetailsPageConnector.js @@ -4,25 +4,42 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { push } from 'connected-react-router'; -import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import NotFound from 'Components/NotFound'; import MovieDetailsConnector from './MovieDetailsConnector'; +import styles from './MovieDetails.css'; function createMapStateToProps() { return createSelector( (state, { match }) => match, - createAllMoviesSelector(), - (match, allMovies) => { + (state) => state.movies, + (match, movies) => { const titleSlug = match.params.titleSlug; - const movieIndex = _.findIndex(allMovies, { titleSlug }); + const { + isFetching, + isPopulated, + error, + items + } = movies; + + const movieIndex = _.findIndex(items, { titleSlug }); if (movieIndex > -1) { return { + isFetching, + isPopulated, titleSlug }; } - return {}; + return { + isFetching, + isPopulated, + error + }; } ); } @@ -48,9 +65,30 @@ class MovieDetailsPageConnector extends Component { render() { const { - titleSlug + titleSlug, + isFetching, + isPopulated, + error } = this.props; + if (isFetching && !isPopulated) { + return ( + + + + + + ); + } + + if (!isFetching && !!error) { + return ( +
+ {getErrorMessage(error, 'Failed to load movie from API')} +
+ ); + } + if (!titleSlug) { return ( { - this.setState({ contentBody: ref }); + setScrollerRef = (ref) => { + this.setState({ scroller: ref }); } getSelectedIds = () => { + if (this.state.allUnselected) { + return []; + } return getSelectedIds(this.state.selectedState); } @@ -164,28 +165,39 @@ class MovieIndex extends Component { // Reset if not sorting by sortTitle if (sortKey !== 'sortTitle') { - this.setState({ jumpBarItems: [] }); + this.setState({ jumpBarItems: { order: [] } }); return; } const characters = _.reduce(items, (acc, item) => { - const firstCharacter = item.sortTitle.charAt(0); + let char = item.sortTitle.charAt(0); + + if (!isNaN(char)) { + char = '#'; + } - if (isNaN(firstCharacter)) { - acc.push(firstCharacter); + if (char in acc) { + acc[char] = acc[char] + 1; } else { - acc.push('#'); + acc[char] = 1; } return acc; - }, []).sort(); + }, {}); + + const order = Object.keys(characters).sort(); // Reverse if sorting descending if (sortDirection === sortDirections.DESCENDING) { - characters.reverse(); + order.reverse(); } - this.setState({ jumpBarItems: _.sortedUniq(characters) }); + const jumpBarItems = { + characters, + order + }; + + this.setState({ jumpBarItems }); } // @@ -275,27 +287,6 @@ class MovieIndex extends Component { this.setState({ isConfirmSearchModalOpen: false }); } - onRender = () => { - this.setState({ isRendered: true }, () => { - const { - scrollTop, - isSmallScreen - } = this.props; - - if (isSmallScreen) { - // Seems to result in the view being off by 125px (distance to the top of the page) - // document.documentElement.scrollTop = document.body.scrollTop = scrollTop; - - // This works, but then jumps another 1px after scrolling - document.documentElement.scrollTop = scrollTop; - } - }); - } - - onScroll = ({ scrollTop }) => { - this.props.onScroll({ scrollTop }); - } - // // Render @@ -321,7 +312,7 @@ class MovieIndex extends Component { saveError, isDeleting, deleteError, - scrollTop, + onScroll, onSortSelect, onFilterSelect, onViewSelect, @@ -332,7 +323,7 @@ class MovieIndex extends Component { } = this.props; const { - contentBody, + scroller, jumpBarItems, jumpToCharacter, isPosterOptionsModalOpen, @@ -340,7 +331,6 @@ class MovieIndex extends Component { isInteractiveImportModalOpen, isConfirmSearchModalOpen, isMovieEditorActive, - isRendered, selectedState, allSelected, allUnselected @@ -349,7 +339,7 @@ class MovieIndex extends Component { const selectedMovieIds = this.getSelectedIds(); const ViewComponent = getViewComponent(view); - const isLoaded = !!(!error && isPopulated && items.length && contentBody); + const isLoaded = !!(!error && isPopulated && items.length && scroller); const hasNoMovie = !totalItems; return ( @@ -489,11 +479,10 @@ class MovieIndex extends Component {
{ isFetching && !isPopulated && @@ -502,21 +491,21 @@ class MovieIndex extends Component { { !isFetching && !!error && -
Unable to load movies
+
+ {getErrorMessage(error, 'Failed to load movie from API')} +
} { isLoaded &&
{ - isLoaded && !!jumpBarItems.length && + isLoaded && !!jumpBarItems.order.length && { // Reset the scroll position before changing the view - this.setState({ scrollTop: 0 }, () => { - this.props.dispatchSetMovieView(view); - }); + this.props.dispatchSetMovieView(view); } onSaveSelected = (payload) => { @@ -152,11 +109,7 @@ class MovieIndexConnector extends Component { } onScroll = ({ scrollTop }) => { - this.setState({ - scrollTop - }, () => { - scrollPositions.movieIndex = scrollTop; - }); + scrollPositions.movieIndex = scrollTop; } // @@ -166,7 +119,6 @@ class MovieIndexConnector extends Component { return ( +
@@ -247,7 +246,6 @@ class MovieIndexOverview extends Component { } MovieIndexOverview.propTypes = { - style: PropTypes.object.isRequired, id: PropTypes.number.isRequired, title: PropTypes.string.isRequired, overview: PropTypes.string.isRequired, diff --git a/frontend/src/Movie/Index/Overview/MovieIndexOverviews.css b/frontend/src/Movie/Index/Overview/MovieIndexOverviews.css index 9c6520fb5..5f8ca5baf 100644 --- a/frontend/src/Movie/Index/Overview/MovieIndexOverviews.css +++ b/frontend/src/Movie/Index/Overview/MovieIndexOverviews.css @@ -1,3 +1,11 @@ .grid { flex: 1 0 auto; } + +.container { + &:hover { + .content { + background-color: $tableRowHoverBackgroundColor; + } + } +} diff --git a/frontend/src/Movie/Index/Overview/MovieIndexOverviews.js b/frontend/src/Movie/Index/Overview/MovieIndexOverviews.js index d16f7cc68..8d9f47032 100644 --- a/frontend/src/Movie/Index/Overview/MovieIndexOverviews.js +++ b/frontend/src/Movie/Index/Overview/MovieIndexOverviews.js @@ -1,12 +1,9 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; import { Grid, WindowScroller } from 'react-virtualized'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import dimensions from 'Styles/Variables/dimensions'; -import { sortDirections } from 'Helpers/Props'; import Measure from 'Components/Measure'; import MovieIndexItemConnector from 'Movie/Index/MovieIndexItemConnector'; import MovieIndexOverview from './MovieIndexOverview'; @@ -66,56 +63,44 @@ class MovieIndexOverviews extends Component { rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}) }; - this._isInitialized = false; this._grid = null; } - componentDidMount() { - this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); - } - - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps, prevState) { const { items, - filters, sortKey, - sortDirection, overviewOptions, jumpToCharacter } = this.props; - const itemsChanged = hasDifferentItems(prevProps.items, items); - const overviewOptionsChanged = !_.isMatch(prevProps.overviewOptions, overviewOptions); + const { + width, + rowHeight + } = this.state; - if ( - prevProps.sortKey !== sortKey || - prevProps.overviewOptions !== overviewOptions || - itemsChanged - ) { + if (prevProps.sortKey !== sortKey || + prevProps.overviewOptions !== overviewOptions) { this.calculateGrid(); } - if ( - prevProps.filters !== filters || - prevProps.sortKey !== sortKey || - prevProps.sortDirection !== sortDirection || - itemsChanged || - overviewOptionsChanged - ) { + if (this._grid && + (prevState.width !== width || + prevState.rowHeight !== rowHeight || + hasDifferentItemsOrOrder(prevProps.items, items))) { + // recomputeGridSize also forces Grid to discard its cache of rendered cells this._grid.recomputeGridSize(); } if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { const index = getIndexOfFirstCharacter(items, jumpToCharacter); - if (index != null) { - const { - rowHeight - } = this.state; + if (this._grid && index != null) { - const scrollTop = rowHeight * index; - - this.props.onScroll({ scrollTop }); + this._grid.scrollToCell({ + rowIndex: index, + columnIndex: 0 + }); } } } @@ -123,21 +108,6 @@ class MovieIndexOverviews extends Component { // // Control - scrollToFirstCharacter(character) { - const items = this.props.items; - const { - rowHeight - } = this.state; - - const index = getIndexOfFirstCharacter(items, character); - - if (index != null) { - const scrollTop = rowHeight * index; - - this.props.onScroll({ scrollTop }); - } - } - setGridRef = (ref) => { this._grid = ref; } @@ -188,26 +158,31 @@ class MovieIndexOverviews extends Component { } return ( - + > + +
); } @@ -218,22 +193,14 @@ class MovieIndexOverviews extends Component { this.calculateGrid(width, this.props.isSmallScreen); } - onSectionRendered = () => { - if (!this._isInitialized && this._contentBodyNode) { - this.props.onRender(); - this._isInitialized = true; - } - } - // // Render render() { const { - items, - scrollTop, isSmallScreen, - onScroll, + scroller, + items, selectedState } = this.props; @@ -243,29 +210,39 @@ class MovieIndexOverviews extends Component { } = this.state; return ( - + - {({ height, isScrolling }) => { + {({ height, registerChild, onChildScroll, scrollTop }) => { + if (!height) { + return
; + } + return ( - +
+ +
); } } @@ -277,20 +254,15 @@ class MovieIndexOverviews extends Component { MovieIndexOverviews.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, - sortDirection: PropTypes.oneOf(sortDirections.all), overviewOptions: PropTypes.object.isRequired, - scrollTop: PropTypes.number.isRequired, jumpToCharacter: PropTypes.string, - contentBody: PropTypes.object.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, showRelativeDates: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired, isSmallScreen: PropTypes.bool.isRequired, timeFormat: PropTypes.string.isRequired, - onRender: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired, selectedState: PropTypes.object.isRequired, onSelectedChange: PropTypes.func.isRequired, isMovieEditorActive: PropTypes.bool.isRequired diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPoster.css b/frontend/src/Movie/Index/Posters/MovieIndexPoster.css index f7815c7ba..65f34c4c8 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPoster.css +++ b/frontend/src/Movie/Index/Posters/MovieIndexPoster.css @@ -1,9 +1,5 @@ $hoverScale: 1.05; -.container { - padding: 10px; -} - .content { transition: all 200ms ease-in; diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPoster.js b/frontend/src/Movie/Index/Posters/MovieIndexPoster.js index 1d15f30a7..624749cbb 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPoster.js +++ b/frontend/src/Movie/Index/Posters/MovieIndexPoster.js @@ -76,7 +76,6 @@ class MovieIndexPoster extends Component { render() { const { - style, id, title, monitored, @@ -119,11 +118,10 @@ class MovieIndexPoster extends Component { }; return ( -
-
-
- { - isMovieEditorActive && +
+
+ { + isMovieEditorActive &&
- } - + } + { - showTitle && -
- {title} -
- } - - { - showMonitored && -
- {monitored ? 'Monitored' : 'Unmonitored'} -
+ status === 'ended' && +
} - { - showQualityProfile && -
- {qualityProfile.name} -
- } + + - - - - - + { + hasPosterError && +
+ {title} +
+ } +
+ + + + { + showTitle && +
+ {title} +
+ } + + { + showMonitored && +
+ {monitored ? 'Monitored' : 'Unmonitored'} +
+ } + + { + showQualityProfile && +
+ {qualityProfile.name} +
+ } + + + + + +
); } } MovieIndexPoster.propTypes = { - style: PropTypes.object.isRequired, id: PropTypes.number.isRequired, title: PropTypes.string.isRequired, monitored: PropTypes.bool.isRequired, diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPosters.css b/frontend/src/Movie/Index/Posters/MovieIndexPosters.css index 9c6520fb5..d80f951a0 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPosters.css +++ b/frontend/src/Movie/Index/Posters/MovieIndexPosters.css @@ -1,3 +1,7 @@ .grid { flex: 1 0 auto; } + +.container { + padding: 10px; +} diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPosters.js b/frontend/src/Movie/Index/Posters/MovieIndexPosters.js index f4f23ab6f..5e7ec012d 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPosters.js +++ b/frontend/src/Movie/Index/Posters/MovieIndexPosters.js @@ -1,11 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; import { Grid, WindowScroller } from 'react-virtualized'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; -import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import dimensions from 'Styles/Variables/dimensions'; -import { sortDirections } from 'Helpers/Props'; import Measure from 'Components/Measure'; import MovieIndexItemConnector from 'Movie/Index/MovieIndexItemConnector'; import MovieIndexPoster from './MovieIndexPoster'; @@ -108,52 +106,46 @@ class MovieIndexPosters extends Component { this._grid = null; } - componentDidMount() { - this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); - } - - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps, prevState) { const { items, - filters, sortKey, - sortDirection, posterOptions, jumpToCharacter } = this.props; - const itemsChanged = hasDifferentItems(prevProps.items, items); + const { + width, + columnWidth, + columnCount, + rowHeight + } = this.state; - if ( - prevProps.sortKey !== sortKey || - prevProps.posterOptions !== posterOptions || - itemsChanged - ) { + if (prevProps.sortKey !== sortKey || + prevProps.posterOptions !== posterOptions) { this.calculateGrid(); } - if ( - prevProps.filters !== filters || - prevProps.sortKey !== sortKey || - prevProps.sortDirection !== sortDirection || - itemsChanged - ) { + if (this._grid && + (prevState.width !== width || + prevState.columnWidth !== columnWidth || + prevState.columnCount !== columnCount || + prevState.rowHeight !== rowHeight || + hasDifferentItemsOrOrder(prevProps.items, items))) { + // recomputeGridSize also forces Grid to discard its cache of rendered cells this._grid.recomputeGridSize(); } if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { const index = getIndexOfFirstCharacter(items, jumpToCharacter); - if (index != null) { - const { - columnCount, - rowHeight - } = this.state; - + if (this._grid && index != null) { const row = Math.floor(index / columnCount); - const scrollTop = rowHeight * row; - this.props.onScroll({ scrollTop }); + this._grid.scrollToCell({ + rowIndex: row, + columnIndex: 0 + }); } } } @@ -214,33 +206,39 @@ class MovieIndexPosters extends Component { showQualityProfile } = posterOptions; - const movie = items[rowIndex * columnCount + columnIndex]; + const movieIdx = rowIndex * columnCount + columnIndex; + const movie = items[movieIdx]; if (!movie) { return null; } return ( - + > + +
); } @@ -251,22 +249,14 @@ class MovieIndexPosters extends Component { this.calculateGrid(width, this.props.isSmallScreen); } - onSectionRendered = () => { - if (!this._isInitialized && this._contentBodyNode) { - this.props.onRender(); - this._isInitialized = true; - } - } - // // Render render() { const { - items, - scrollTop, isSmallScreen, - onScroll, + scroller, + items, selectedState } = this.props; @@ -280,29 +270,39 @@ class MovieIndexPosters extends Component { const rowCount = Math.ceil(items.length / columnCount); return ( - + - {({ height, isScrolling }) => { + {({ height, registerChild, onChildScroll, scrollTop }) => { + if (!height) { + return
; + } + return ( - +
+ +
); } } @@ -314,19 +314,14 @@ class MovieIndexPosters extends Component { MovieIndexPosters.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, - sortDirection: PropTypes.oneOf(sortDirections.all), posterOptions: PropTypes.object.isRequired, - scrollTop: PropTypes.number.isRequired, jumpToCharacter: PropTypes.string, - contentBody: PropTypes.object.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, showRelativeDates: PropTypes.bool.isRequired, shortDateFormat: PropTypes.string.isRequired, isSmallScreen: PropTypes.bool.isRequired, timeFormat: PropTypes.string.isRequired, - onRender: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired, selectedState: PropTypes.object.isRequired, onSelectedChange: PropTypes.func.isRequired, isMovieEditorActive: PropTypes.bool.isRequired diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.css b/frontend/src/Movie/Index/Table/MovieIndexRow.css index 03179a787..c8a33b6fd 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.css +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.css @@ -79,6 +79,7 @@ composes: cell; flex: 0 1 90px; + min-width: 60px; } .checkInput { diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.js b/frontend/src/Movie/Index/Table/MovieIndexRow.js index c7c47bdc7..994cf660a 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.js +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.js @@ -1,15 +1,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -// import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; import titleCase from 'Utilities/String/titleCase'; import { icons } from 'Helpers/Props'; import HeartRating from 'Components/HeartRating'; import IconButton from 'Components/Link/IconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; -// import ProgressBar from 'Components/ProgressBar'; import TagListConnector from 'Components/TagListConnector'; -// import CheckInput from 'Components/Form/CheckInput'; -import VirtualTableRow from 'Components/Table/VirtualTableRow'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import MovieTitleLink from 'Movie/MovieTitleLink'; @@ -63,7 +59,6 @@ class MovieIndexRow extends Component { render() { const { - style, id, monitored, status, @@ -97,7 +92,7 @@ class MovieIndexRow extends Component { } = this.state; return ( - + <> { columns.map((column) => { const { @@ -339,13 +334,12 @@ class MovieIndexRow extends Component { movieId={id} onModalClose={this.onDeleteMovieModalClose} /> - + ); } } MovieIndexRow.propTypes = { - style: PropTypes.object.isRequired, id: PropTypes.number.isRequired, monitored: PropTypes.bool.isRequired, status: PropTypes.string.isRequired, diff --git a/frontend/src/Movie/Index/Table/MovieIndexTable.js b/frontend/src/Movie/Index/Table/MovieIndexTable.js index 1dfa299e2..d3180e9e0 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexTable.js +++ b/frontend/src/Movie/Index/Table/MovieIndexTable.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; import { sortDirections } from 'Helpers/Props'; import VirtualTable from 'Components/Table/VirtualTable'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; import MovieIndexItemConnector from 'Movie/Index/MovieIndexItemConnector'; import MovieIndexHeaderConnector from './MovieIndexHeaderConnector'; import MovieIndexRow from './MovieIndexRow'; @@ -23,11 +24,10 @@ class MovieIndexTable extends Component { componentDidUpdate(prevProps) { const { - items + items, + jumpToCharacter } = this.props; - const jumpToCharacter = this.props.jumpToCharacter; - if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) { const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter); @@ -55,17 +55,21 @@ class MovieIndexTable extends Component { const movie = items[rowIndex]; return ( - + > + + ); } @@ -76,15 +80,11 @@ class MovieIndexTable extends Component { const { items, columns, - filters, sortKey, sortDirection, isSmallScreen, - scrollTop, - contentBody, onSortPress, - onRender, - onScroll, + scroller, allSelected, allUnselected, onSelectAllChange, @@ -96,10 +96,9 @@ class MovieIndexTable extends Component { ); } @@ -130,16 +124,12 @@ class MovieIndexTable extends Component { MovieIndexTable.propTypes = { items: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired, - filters: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, sortDirection: PropTypes.oneOf(sortDirections.all), - scrollTop: PropTypes.number.isRequired, jumpToCharacter: PropTypes.string, - contentBody: PropTypes.object.isRequired, isSmallScreen: PropTypes.bool.isRequired, + scroller: PropTypes.instanceOf(Element).isRequired, onSortPress: PropTypes.func.isRequired, - onRender: PropTypes.func.isRequired, - onScroll: PropTypes.func.isRequired, allSelected: PropTypes.bool.isRequired, allUnselected: PropTypes.bool.isRequired, selectedState: PropTypes.object.isRequired, diff --git a/frontend/src/Store/Actions/Creators/createHandleActions.js b/frontend/src/Store/Actions/Creators/createHandleActions.js index c3315ce94..0883f8e08 100644 --- a/frontend/src/Store/Actions/Creators/createHandleActions.js +++ b/frontend/src/Store/Actions/Creators/createHandleActions.js @@ -42,6 +42,7 @@ export default function createHandleActions(handlers, defaultState, section) { if (_.isArray(payload.data)) { newState.items = payload.data; + newState.itemMap = _.zipObject(_.map(payload.data, 'id'), _.range(payload.data.length)); } else { newState.item = payload.data; } @@ -75,6 +76,7 @@ export default function createHandleActions(handlers, defaultState, section) { newState.items.splice(index, 1, { ...item, ...otherProps }); } else if (!updateOnly) { newState.items.push({ ...otherProps }); + newState.itemMap = _.zipObject(_.map(newState.items, 'id'), _.range(newState.items.length)); } return updateSectionState(state, payloadSection, newState); @@ -111,6 +113,8 @@ export default function createHandleActions(handlers, defaultState, section) { newState.items = [...newState.items]; _.remove(newState.items, { id: payload.id }); + newState.itemMap = _.zipObject(_.map(newState.items, 'id'), _.range(newState.items.length)); + return updateSectionState(state, payloadSection, newState); } diff --git a/frontend/src/Store/Middleware/createSentryMiddleware.js b/frontend/src/Store/Middleware/createSentryMiddleware.js index 02f6163cd..23eb89b3b 100644 --- a/frontend/src/Store/Middleware/createSentryMiddleware.js +++ b/frontend/src/Store/Middleware/createSentryMiddleware.js @@ -1,5 +1,6 @@ import _ from 'lodash'; import * as sentry from '@sentry/browser'; +import * as Integrations from '@sentry/integrations'; import parseUrl from 'Utilities/String/parseUrl'; function cleanseUrl(url) { @@ -34,6 +35,13 @@ function identity(stuff) { return stuff; } +function stripUrlBase(frame) { + if (frame.filename && window.Radarr.urlBase) { + frame.filename = frame.filename.replace(window.Radarr.urlBase, ''); + } + return frame; +} + function createMiddleware() { return (store) => (next) => (action) => { try { @@ -80,7 +88,8 @@ export default function createSentryMiddleware() { environment: branch, release, sendDefaultPii: true, - beforeSend: cleanseData + beforeSend: cleanseData, + integrations: [new Integrations.RewriteFrames({ iteratee: stripUrlBase })] }); sentry.configureScope((scope) => { diff --git a/frontend/src/Store/Selectors/createMovieCountSelector.js b/frontend/src/Store/Selectors/createMovieCountSelector.js index e8e76eaa4..aa87d792e 100644 --- a/frontend/src/Store/Selectors/createMovieCountSelector.js +++ b/frontend/src/Store/Selectors/createMovieCountSelector.js @@ -4,8 +4,12 @@ import createAllMoviesSelector from './createAllMoviesSelector'; function createMovieCountSelector() { return createSelector( createAllMoviesSelector(), - (movies) => { - return movies.length; + (state) => state.movies.error, + (movies, error) => { + return { + count: movies.length, + error + }; } ); } diff --git a/frontend/src/Store/Selectors/createMovieSelector.js b/frontend/src/Store/Selectors/createMovieSelector.js index 624b67a78..caf55d7d3 100644 --- a/frontend/src/Store/Selectors/createMovieSelector.js +++ b/frontend/src/Store/Selectors/createMovieSelector.js @@ -1,12 +1,15 @@ import { createSelector } from 'reselect'; -import createAllMoviesSelector from './createAllMoviesSelector'; function createMovieSelector() { return createSelector( (state, { movieId }) => movieId, - createAllMoviesSelector(), - (movieId, allMovies) => { - return allMovies.find((movie) => movie.id === movieId); + (state) => state.movies.itemMap, + (state) => state.movies.items, + (movieId, itemMap, allMovies) => { + if (allMovies && itemMap && movieId in itemMap) { + return allMovies[itemMap[movieId]]; + } + return undefined; } ); } diff --git a/frontend/src/Utilities/Object/hasDifferentItems.js b/frontend/src/Utilities/Object/hasDifferentItems.js index f89c99a10..826aab7c3 100644 --- a/frontend/src/Utilities/Object/hasDifferentItems.js +++ b/frontend/src/Utilities/Object/hasDifferentItems.js @@ -1,10 +1,19 @@ -import _ from 'lodash'; - function hasDifferentItems(prevItems, currentItems, idProp = 'id') { - const diff1 = _.differenceBy(prevItems, currentItems, (item) => item[idProp]); - const diff2 = _.differenceBy(currentItems, prevItems, (item) => item[idProp]); + if (prevItems === currentItems) { + return false; + } + + if (prevItems.length !== currentItems.length) { + return true; + } + + const currentItemIds = new Set(); + + currentItems.forEach((currentItem) => { + currentItemIds.add(currentItem[idProp]); + }); - return diff1.length > 0 || diff2.length > 0; + return prevItems.every((prevItem) => currentItemIds.has(prevItem[idProp])); } export default hasDifferentItems; diff --git a/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js b/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js new file mode 100644 index 000000000..e2acbc5c0 --- /dev/null +++ b/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js @@ -0,0 +1,21 @@ +function hasDifferentItemsOrOrder(prevItems, currentItems, idProp = 'id') { + if (prevItems === currentItems) { + return false; + } + + const len = prevItems.length; + + if (len !== currentItems.length) { + return true; + } + + for (let i = 0; i < len; i++) { + if (prevItems[i][idProp] !== currentItems[i][idProp]) { + return true; + } + } + + return false; +} + +export default hasDifferentItemsOrOrder; diff --git a/frontend/src/index.html b/frontend/src/index.html index 069609647..ed138adb0 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -47,8 +47,8 @@ content="/Content/Images/Icons/browserconfig.xml" /> - - + + Radarr (Preview) @@ -80,7 +80,5 @@ - - - + diff --git a/frontend/src/index.js b/frontend/src/index.js index 9f67578ad..e03b26dfa 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,3 +1,6 @@ +/* eslint-disable-next-line no-undef */ +__webpack_public_path__ = `${window.Radarr.urlBase}/`; + 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 deleted file mode 100644 index 2b4902141..000000000 --- a/frontend/src/preload.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint no-undef: 0 */ -__webpack_public_path__ = `${window.Radarr.urlBase}/`; diff --git a/frontend/src/vendor.js b/frontend/src/vendor.js deleted file mode 100644 index 2b08817be..000000000 --- a/frontend/src/vendor.js +++ /dev/null @@ -1,5 +0,0 @@ -/* Base */ -// require('jquery'); -require('lodash'); -require('moment'); -// require('signalR'); diff --git a/package.json b/package.json index 41f8f7297..73af5ab96 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "gulp-watch": "5.0.1", "gulp-wrap": "0.15.0", "history": "4.9.0", + "html-webpack-plugin": "3.2.0", "jdu": "1.0.0", "jquery": "3.4.1", "loader-utils": "^1.1.0", @@ -122,7 +123,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/src/NzbDrone.Core/Datastore/Migration/161_speed_improvements.cs b/src/NzbDrone.Core/Datastore/Migration/161_speed_improvements.cs new file mode 100644 index 000000000..cd43e493e --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/161_speed_improvements.cs @@ -0,0 +1,21 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(161)] + public class speed_improvements : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + // Auto indices SQLite is creating + Create.Index("IX_MovieFiles_MovieId").OnTable("MovieFiles").OnColumn("MovieId"); + Create.Index("IX_AlternativeTitles_MovieId").OnTable("AlternativeTitles").OnColumn("MovieId"); + + // Speed up release processing (these are present in Sonarr) + Create.Index("IX_Movies_CleanTitle").OnTable("Movies").OnColumn("CleanTitle"); + Create.Index("IX_Movies_ImdbId").OnTable("Movies").OnColumn("ImdbId"); + Create.Index("IX_Movies_TmdbId").OnTable("Movies").OnColumn("TmdbId"); + } + } +} diff --git a/src/NzbDrone.Core/Movies/MovieRepository.cs b/src/NzbDrone.Core/Movies/MovieRepository.cs index 7d11891f5..e661aefe0 100644 --- a/src/NzbDrone.Core/Movies/MovieRepository.cs +++ b/src/NzbDrone.Core/Movies/MovieRepository.cs @@ -9,7 +9,6 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Movies.AlternativeTitles; using NzbDrone.Core.Parser.RomanNumerals; using NzbDrone.Core.Qualities; -using CoreParser = NzbDrone.Core.Parser.Parser; namespace NzbDrone.Core.Movies { @@ -18,6 +17,7 @@ namespace NzbDrone.Core.Movies bool MoviePathExists(string path); Movie FindByTitle(string cleanTitle); Movie FindByTitle(string cleanTitle, int year); + List FindByTitleInexact(string cleanTitle); Movie FindByImdbId(string imdbid); Movie FindByTmdbId(int tmdbid); Movie FindByTitleSlug(string slug); @@ -169,7 +169,6 @@ namespace NzbDrone.Core.Movies string cleanTitleWithRomanNumbers = cleanTitle; string cleanTitleWithArabicNumbers = cleanTitle; - foreach (ArabicRomanNumeral arabicRomanNumeral in RomanNumeralParser.GetArabicRomanNumeralsMapping()) { string arabicNumber = arabicRomanNumeral.ArabicNumeralAsString; @@ -182,23 +181,27 @@ namespace NzbDrone.Core.Movies if (result == null) { - result = - Query.Where(movie => movie.CleanTitle == cleanTitleWithArabicNumbers).FirstWithYear(year) ?? - Query.Where(movie => movie.CleanTitle == cleanTitleWithRomanNumbers).FirstWithYear(year); + result = Query.Where(movie => movie.CleanTitle == cleanTitleWithArabicNumbers || movie.CleanTitle == cleanTitleWithRomanNumbers) + .FirstWithYear(year); if (result == null) { - - result = Query.Join(JoinType.Inner, m => m.AlternativeTitles, (m, t) => m.Id == t.MovieId) - .Where(t => t.CleanTitle == cleanTitle || t.CleanTitle == cleanTitleWithArabicNumbers || t.CleanTitle == cleanTitleWithRomanNumbers) + result = Query.Where(t => t.CleanTitle == cleanTitle || t.CleanTitle == cleanTitleWithArabicNumbers || t.CleanTitle == cleanTitleWithRomanNumbers) .FirstWithYear(year); - } } return result; } + public List FindByTitleInexact(string cleanTitle) + { + var mapper = _database.GetDataMapper(); + mapper.AddParameter("queryTitle", cleanTitle); + + return AddJoinQueries(mapper.Query()).Where($"instr(@queryTitle, [t0].[CleanTitle])"); + } + public Movie FindByTmdbId(int tmdbid) { return Query.Where(m => m.TmdbId == tmdbid).FirstOrDefault(); diff --git a/src/NzbDrone.Core/Movies/MovieService.cs b/src/NzbDrone.Core/Movies/MovieService.cs index c5004599d..0ff2e1b24 100644 --- a/src/NzbDrone.Core/Movies/MovieService.cs +++ b/src/NzbDrone.Core/Movies/MovieService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -26,6 +25,7 @@ namespace NzbDrone.Core.Movies Movie AddMovie(Movie newMovie); List AddMovies(List newMovies); Movie FindByImdbId(string imdbid); + Movie FindByTmdbId(int tmdbid); Movie FindByTitle(string title); Movie FindByTitle(string title, int year); Movie FindByTitleInexact(string title, int? year); @@ -58,7 +58,6 @@ namespace NzbDrone.Core.Movies private readonly IImportExclusionsService _exclusionService; private readonly Logger _logger; - public MovieService(IMovieRepository movieRepository, IEventAggregator eventAggregator, IBuildFileNames fileNameBuilder, @@ -167,7 +166,7 @@ namespace NzbDrone.Core.Movies newMovie.PathState = defaultState == MoviePathState.Dynamic ? MoviePathState.StaticOnce : MoviePathState.Static; } - _logger.Info("Adding Movie {0} Path: [{1}]", newMovie, newMovie.Path); + _logger.Info("Adding Movie {0} Path: [{1}]", newMovie, newMovie.Path); newMovie.CleanTitle = newMovie.Title.CleanSeriesTitle(); newMovie.SortTitle = MovieTitleNormalizer.Normalize(newMovie.Title, newMovie.TmdbId); @@ -232,12 +231,16 @@ namespace NzbDrone.Core.Movies return _movieRepository.FindByImdbId(imdbid); } + public Movie FindByTmdbId(int tmdbid) + { + return _movieRepository.FindByTmdbId(tmdbid); + } + private List FindByTitleInexactAll(string title) { // find any movie clean title within the provided release title string cleanTitle = title.CleanSeriesTitle(); - var list = _movieRepository.All().Where(s => cleanTitle.Contains(s.CleanTitle)) - .Union(_movieRepository.All().Where(s => s.CleanTitle.Contains(cleanTitle))).ToList(); + var list = _movieRepository.FindByTitleInexact(cleanTitle); if (!list.Any()) { // no movie matched @@ -258,8 +261,6 @@ namespace NzbDrone.Core.Movies .Select(s => s.movie) .ToList(); - - return query; } diff --git a/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs b/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs index ff5c4786a..5b4a7745e 100644 --- a/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/MovieExistsValidator.cs @@ -1,4 +1,3 @@ -using System; using FluentValidation.Validators; using NzbDrone.Core.Movies; @@ -20,7 +19,7 @@ namespace NzbDrone.Core.Validation.Paths int tmdbId = (int)context.PropertyValue; - return (!_movieService.GetAllMovies().Exists(s => s.TmdbId == tmdbId)); + return (_movieService.FindByTmdbId(tmdbId) == null); } } } diff --git a/yarn.lock b/yarn.lock index 13ae7e989..516f43533 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1838,6 +1838,11 @@ beeper@^1.0.0: resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" integrity sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak= +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -1883,6 +1888,11 @@ body@^5.1.0: raw-body "~1.1.0" safe-json-parse "~1.0.1" +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2109,6 +2119,14 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camel-case@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + camelcase-css@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" @@ -2252,6 +2270,13 @@ classnames@2.2.6, classnames@^2.2.0: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== +clean-css@4.2.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" + integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g== + dependencies: + source-map "~0.6.0" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -2400,11 +2425,21 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@2.17.x: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== + commander@^2.2.0, commander@^2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@~2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -2658,6 +2693,21 @@ css-loader@3.0.0: postcss-value-parser "^4.0.0" schema-utils "^1.0.0" +css-select@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + css@2.X, css@^2.2.1: version "2.2.4" resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" @@ -2965,6 +3015,13 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-converter@^0.2: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + dom-css@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/dom-css/-/dom-css-2.1.0.tgz#fdbc2d5a015d0a3e1872e11472bbd0e7b9e6a202" @@ -3011,6 +3068,14 @@ domhandler@^2.3.0: dependencies: domelementtype "1" +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + domutils@^1.5.1: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -3169,6 +3234,22 @@ es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.7.0: is-regex "^1.0.4" object-keys "^1.0.12" +es-abstract@^1.5.1: + version "1.16.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" + integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.0" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-inspect "^1.6.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" + es-to-primitive@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" @@ -4482,6 +4563,11 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +he@1.2.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + history@4.9.0, history@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" @@ -4522,12 +4608,38 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546" integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ== +html-minifier@^3.2.3: + version "3.5.21" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" + integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== + dependencies: + camel-case "3.0.x" + clean-css "4.2.x" + commander "2.17.x" + he "1.2.x" + param-case "2.1.x" + relateurl "0.2.x" + uglify-js "3.4.x" + html-tags@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== -htmlparser2@^3.10.0: +html-webpack-plugin@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" + integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s= + dependencies: + html-minifier "^3.2.3" + loader-utils "^0.2.16" + lodash "^4.17.3" + pretty-error "^2.0.2" + tapable "^1.0.0" + toposort "^1.0.0" + util.promisify "1.0.0" + +htmlparser2@^3.10.0, htmlparser2@^3.3.0: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== @@ -5195,6 +5307,11 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json5@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -5378,7 +5495,17 @@ loader-runner@^2.3.0, loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.2, loader-utils@^1.2.3: +loader-utils@^0.2.16: + version "0.2.17" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" + integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + +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== @@ -5547,7 +5674,7 @@ lodash@4.17.14: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== -lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.4: +lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.3, lodash@^4.17.4: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -5593,6 +5720,11 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -6077,6 +6209,13 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +no-case@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== + dependencies: + lower-case "^1.1.1" + node-fetch@^1.0.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -6274,6 +6413,13 @@ nsdeclare@^0.1.0: resolved "https://registry.yarnpkg.com/nsdeclare/-/nsdeclare-0.1.0.tgz#10daa153642382d3cf2c01a916f4eb20a128b19f" integrity sha1-ENqhU2QjgtPPLAGpFvTrIKEosZ8= +nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -6308,7 +6454,12 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-keys@^1.0.11, object-keys@^1.0.12: +object-inspect@^1.6.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -6365,6 +6516,14 @@ object.fromentries@^2.0.0: function-bind "^1.1.1" has "^1.0.1" +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + object.map@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" @@ -6547,6 +6706,13 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" +param-case@2.1.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" + integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= + dependencies: + no-case "^2.2.0" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -7079,6 +7245,14 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= +pretty-error@^2.0.2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" + integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= + dependencies: + renderkid "^2.0.1" + utila "~0.4" + pretty-hrtime@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -7744,6 +7918,11 @@ regjsparser@^0.6.0: dependencies: jsesc "~0.5.0" +relateurl@0.2.x: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + remark-parse@^6.0.0: version "6.0.3" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-6.0.3.tgz#c99131052809da482108413f87b0ee7f52180a3a" @@ -7816,6 +7995,17 @@ remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0: resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= +renderkid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" + integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== + dependencies: + css-select "^1.1.0" + dom-converter "^0.2" + htmlparser2 "^3.3.0" + strip-ansi "^3.0.0" + utila "^0.4.0" + repeat-element@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" @@ -8125,6 +8315,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" @@ -8535,6 +8733,22 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^5.2.0" +string.prototype.trimleft@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" + integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" + integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + string_decoder@0.10, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -9009,6 +9223,11 @@ to-through@^2.0.0: dependencies: through2 "^2.0.3" +toposort@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" + integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= + tough-cookie@~2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" @@ -9101,6 +9320,14 @@ ua-parser-js@^0.7.18: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098" integrity sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw== +uglify-js@3.4.x: + version "3.4.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" + integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== + dependencies: + commander "~2.19.0" + source-map "~0.6.1" + unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -9259,6 +9486,11 @@ upath@^1.1.1: resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== +upper-case@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -9313,6 +9545,14 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +util.promisify@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + util@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" @@ -9327,6 +9567,11 @@ util@^0.11.0: dependencies: inherits "2.0.3" +utila@^0.4.0, utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= + uuid@^3.3.2: version "3.3.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" @@ -9666,6 +9911,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"