Fix: Aphrodite UI enhancements

* New: Display UI before movies have loaded

* Revised webpack bundling

* New: Option for production build with profiling

* Fixed: Faster hasDifferentItems and specialized OrOrder version

* Fixed: Faster movie selector

* Fixed: Speed up release processing, add indices (migration 161)

* Fixed: Use a worker for UI fuzzy search

* Fixed: Don't loop over all movies if we know none selected

* Fixed: Strip UrlBase from UI events before sending to sentry

Should mean that source maps are picked up correctly.

* Better selection of jump bar items

Show first, last and most common items

* Fixed: Don't repeatedly re-render cells

* Rework Movie Index and virtualTable

* Corresponding improvements for AddListMovie and ImportMovie
pull/3916/head
ta264 5 years ago committed by Devin Buhl
parent 95e5e3132b
commit abe7a85a39

@ -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('<!-- webpack bundles head -->', head.join('\r\n '))
.replace('<!-- webpack bundles body -->', 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);

@ -53,6 +53,10 @@ class BlacklistRow extends Component {
onRemovePress
} = this.props;
if (!movie) {
return null;
}
return (
<TableRow>
{

@ -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 (
<PageContent title="History">
@ -71,12 +76,12 @@ class History extends Component {
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
isFetchingAny && !isAllPopulated &&
<LoadingIndicator />
}
{
!isFetching && hasError &&
!isFetchingAny && hasError &&
<div>Unable to load history</div>
}
@ -91,7 +96,7 @@ class History extends Component {
}
{
isPopulated && !hasError && !!items.length &&
isAllPopulated && !hasError && !!items.length &&
<div>
<Table
columns={columns}
@ -130,6 +135,9 @@ History.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isMoviesFetching: PropTypes.bool.isRequired,
isMoviesPopulated: PropTypes.bool.isRequired,
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired,

@ -10,8 +10,12 @@ import History from './History';
function createMapStateToProps() {
return createSelector(
(state) => state.history,
(history) => {
(state) => state.movies,
(history, movies) => {
return {
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
...history
};
}

@ -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 {
<PageContentBodyConnector>
{
isRefreshing && !isPopulated &&
isRefreshing && !isAllPopulated &&
<LoadingIndicator />
}
@ -194,7 +198,7 @@ class Queue extends Component {
}
{
isPopulated && !hasError && !!items.length &&
isAllPopulated && !hasError && !!items.length &&
<div>
<Table
columns={columns}
@ -247,6 +251,9 @@ Queue.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isMoviesFetching: PropTypes.bool.isRequired,
isMoviesPopulated: PropTypes.bool.isRequired,
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,

@ -12,11 +12,15 @@ import Queue from './Queue';
function createMapStateToProps() {
return createSelector(
(state) => 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

@ -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 (
<AddListMovie
{...this.props}
scrollTop={this.state.scrollTop}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
@ -150,7 +104,6 @@ class AddDiscoverMovieConnector extends Component {
AddDiscoverMovieConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
scrollTop: PropTypes.number.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchListMovies: PropTypes.func.isRequired,
dispatchClearListMovie: PropTypes.func.isRequired,

@ -1,7 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItems';
import { align, icons, sortDirections } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@ -43,15 +43,14 @@ class AddListMovie extends Component {
super(props, context);
this.state = {
contentBody: null,
jumpBarItems: [],
scroller: null,
jumpBarItems: { order: [] },
jumpToCharacter: null,
isPosterOptionsModalOpen: false,
isOverviewOptionsModalOpen: false,
isConfirmSearchModalOpen: false,
searchType: null,
lastToggled: null,
isRendered: false
lastToggled: null
};
}
@ -63,19 +62,17 @@ class AddListMovie extends Component {
const {
items,
sortKey,
sortDirection,
scrollTop
sortDirection
} = this.props;
if (
hasDifferentItems(prevProps.items, items) ||
sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection
if (sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection ||
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setJumpBarItems();
}
if (this.state.jumpToCharacter != null && scrollTop !== prevProps.scrollTop) {
if (this.state.jumpToCharacter != null) {
this.setState({ jumpToCharacter: null });
}
}
@ -83,8 +80,8 @@ class AddListMovie extends Component {
//
// Control
setContentBodyRef = (ref) => {
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 {
<div className={styles.pageContentBodyWrapper}>
<PageContentBodyConnector
ref={this.setContentBodyRef}
registerScroller={this.setScrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
scrollTop={isRendered ? scrollTop : 0}
onScroll={this.onScroll}
onScroll={onScroll}
>
{
isFetching && !isPopulated &&
@ -295,14 +279,12 @@ class AddListMovie extends Component {
isLoaded &&
<div className={styles.contentBodyContainer}>
<ViewComponent
contentBody={contentBody}
scroller={scroller}
items={items}
filters={filters}
sortKey={sortKey}
sortDirection={sortDirection}
scrollTop={scrollTop}
jumpToCharacter={jumpToCharacter}
onRender={this.onRender}
{...otherProps}
/>
</div>
@ -317,7 +299,7 @@ class AddListMovie extends Component {
</PageContentBodyConnector>
{
isLoaded && !!jumpBarItems.length &&
isLoaded && !!jumpBarItems.order.length &&
<PageJumpBar
items={jumpBarItems}
onItemPress={this.onJumpBarItemPress}
@ -352,7 +334,6 @@ AddListMovie.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
view: PropTypes.string.isRequired,
scrollTop: PropTypes.number.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,

@ -3,7 +3,6 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAddMovieClientSideCollectionItemsSelector from 'Store/Selectors/createAddMovieClientSideCollectionItemsSelector';
import dimensions from 'Styles/Variables/dimensions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchListMovies, clearAddMovie, setListMovieSort, setListMovieFilter, setListMovieView, setListMovieTableOption } from 'Store/Actions/addMovieActions';
@ -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'),
@ -85,23 +61,6 @@ function createMapDispatchToProps(dispatch, props) {
class AddListMovieConnector 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 +76,11 @@ class AddListMovieConnector 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 +90,6 @@ class AddListMovieConnector extends Component {
return (
<AddListMovie
{...this.props}
scrollTop={this.state.scrollTop}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
@ -150,7 +101,6 @@ class AddListMovieConnector extends Component {
AddListMovieConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
scrollTop: PropTypes.number.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchListMovies: PropTypes.func.isRequired,
dispatchClearListMovie: PropTypes.func.isRequired,

@ -52,7 +52,6 @@ class AddListMovieOverview extends Component {
render() {
const {
style,
tmdbId,
title,
titleSlug,
@ -82,7 +81,7 @@ class AddListMovieOverview extends Component {
const overviewHeight = contentHeight - titleRowHeight;
return (
<div className={styles.container} style={style}>
<div className={styles.container}>
<Link
className={styles.content}
{...linkProps}
@ -132,7 +131,6 @@ class AddListMovieOverview extends Component {
}
AddListMovieOverview.propTypes = {
style: PropTypes.object.isRequired,
tmdbId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
folder: PropTypes.string.isRequired,

@ -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/hasDifferentItems';
import dimensions from 'Styles/Variables/dimensions';
import { sortDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import AddListMovieItemConnector from 'AddMovie/AddListMovie/AddListMovieItemConnector';
import AddListMovieOverviewConnector from './AddListMovieOverviewConnector';
@ -60,56 +57,44 @@ class AddListMovieOverviews 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;
const scrollTop = rowHeight * index;
if (this._grid && index != null) {
this.props.onScroll({ scrollTop });
this._grid.scrollToCell({
rowIndex: index,
columnIndex: 0
});
}
}
}
@ -117,21 +102,6 @@ class AddListMovieOverviews 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;
}
@ -179,22 +149,27 @@ class AddListMovieOverviews extends Component {
}
return (
<AddListMovieItemConnector
<div
className={styles.container}
key={key}
component={AddListMovieOverviewConnector}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
rowHeight={rowHeight}
overviewOptions={overviewOptions}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
style={style}
movieId={movie.tmdbId}
/>
>
<AddListMovieItemConnector
key={movie.id}
component={AddListMovieOverviewConnector}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
rowHeight={rowHeight}
overviewOptions={overviewOptions}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
movieId={movie.tmdbId}
/>
</div>
);
}
@ -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 (
<Measure onMeasure={this.onMeasure}>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={onScroll}
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, isScrolling }) => {
{({ height, registerChild, onChildScroll, scrollTop }) => {
return (
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
onSectionRendered={this.onSectionRendered}
/>
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptout={true}
/>
</div>
);
}
}
@ -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;

@ -47,7 +47,6 @@ class AddListMoviePoster extends Component {
render() {
const {
style,
tmdbId,
title,
year,
@ -75,67 +74,64 @@ class AddListMoviePoster extends Component {
};
return (
<div className={styles.container} style={style}>
<div className={styles.content}>
<div className={styles.posterContainer}>
{
status === 'ended' &&
<div
className={styles.ended}
title="Ended"
/>
}
<Link
className={styles.link}
style={elementStyle}
{...linkProps}
>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{title}
</div>
}
</Link>
</div>
<div className={styles.content}>
<div className={styles.posterContainer}>
{
showTitle &&
<div className={styles.title}>
{title}
</div>
status === 'ended' &&
<div
className={styles.ended}
title="Ended"
/>
}
<AddNewMovieModal
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
tmdbId={tmdbId}
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddMovieModalClose}
/>
<Link
className={styles.link}
style={elementStyle}
{...linkProps}
>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{title}
</div>
}
</Link>
</div>
{
showTitle &&
<div className={styles.title}>
{title}
</div>
}
<AddNewMovieModal
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
tmdbId={tmdbId}
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddMovieModalClose}
/>
</div>
);
}
}
AddListMoviePoster.propTypes = {
style: PropTypes.object.isRequired,
tmdbId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,

@ -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 (
<AddListMovieItemConnector
<div
key={key}
component={AddListMoviePosterConnector}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
showTitle={showTitle}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
style={style}
movieId={movie.tmdbId}
/>
>
<AddListMovieItemConnector
key={movie.id}
component={AddListMoviePosterConnector}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
showTitle={showTitle}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
movieId={movie.tmdbId}
/>
</div>
);
}
@ -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 (
<Measure onMeasure={this.onMeasure}>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={onScroll}
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, isScrolling }) => {
{({ height, registerChild, onChildScroll, scrollTop }) => {
return (
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
onSectionRendered={this.onSectionRendered}
/>
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
@ -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;

@ -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 (
<div>
<VirtualTableRow style={style}>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<MovieStatusCell
key={name}
className={styles[name]}
// monitored={monitored}
status={status}
component={VirtualTableRowCell}
/>
);
}
if (name === 'sortTitle') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<Link
{...linkProps}
>
{title}
</Link>
</VirtualTableRowCell>
);
}
if (name === 'studio') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
<>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<MovieStatusCell
key={name}
className={styles[name]}
status={status}
component={VirtualTableRowCell}
/>
);
}
if (name === 'sortTitle') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<Link
{...linkProps}
>
{studio}
</VirtualTableRowCell>
);
}
if (name === 'inCinemas') {
return (
<RelativeDateCellConnector
key={name}
className={styles[name]}
date={inCinemas}
component={VirtualTableRowCell}
{title}
</Link>
</VirtualTableRowCell>
);
}
if (name === 'studio') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
{studio}
</VirtualTableRowCell>
);
}
if (name === 'inCinemas') {
return (
<RelativeDateCellConnector
key={name}
className={styles[name]}
date={inCinemas}
component={VirtualTableRowCell}
/>
);
}
if (name === 'physicalRelease') {
return (
<RelativeDateCellConnector
key={name}
className={styles[name]}
date={physicalRelease}
component={VirtualTableRowCell}
/>
);
}
if (name === 'genres') {
const joinedGenres = genres.join(', ');
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<span title={joinedGenres}>
{joinedGenres}
</span>
</VirtualTableRowCell>
);
}
if (name === 'ratings') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<HeartRating
rating={ratings.value}
/>
);
}
if (name === 'physicalRelease') {
return (
<RelativeDateCellConnector
key={name}
className={styles[name]}
date={physicalRelease}
component={VirtualTableRowCell}
/>
);
}
if (name === 'genres') {
const joinedGenres = genres.join(', ');
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<span title={joinedGenres}>
{joinedGenres}
</span>
</VirtualTableRowCell>
);
}
if (name === 'ratings') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<HeartRating
rating={ratings.value}
/>
</VirtualTableRowCell>
);
}
if (name === 'certification') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
{certification}
</VirtualTableRowCell>
);
}
return null;
})
}
<AddNewMovieModal
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
tmdbId={tmdbId}
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddMovieModalClose}
/>
</VirtualTableRow>
</div>
</VirtualTableRowCell>
);
}
if (name === 'certification') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
{certification}
</VirtualTableRowCell>
);
}
return null;
})
}
<AddNewMovieModal
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
tmdbId={tmdbId}
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddMovieModalClose}
/>
</>
);
}
}
AddListMovieRow.propTypes = {
style: PropTypes.object.isRequired,
tmdbId: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,

@ -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 (
<AddListMovieItemConnector
<VirtualTableRow
key={key}
component={AddListMovieRowConnector}
style={style}
columns={columns}
movieId={movie.tmdbId}
/>
>
<AddListMovieItemConnector
key={movie.id}
component={AddListMovieRowConnector}
columns={columns}
movieId={movie.tmdbId}
/>
</VirtualTableRow>
);
}
@ -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 (
<VirtualTable
className={styles.tableContainer}
items={items}
scrollTop={scrollTop}
scrollIndex={this.state.scrollIndex}
contentBody={contentBody}
isSmallScreen={isSmallScreen}
scroller={scroller}
rowHeight={38}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
@ -100,11 +97,6 @@ class AddListMovieTable extends Component {
/>
}
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;

@ -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 (
<PageContent title="Import Movies">
<PageContentBodyConnector
ref={this.setContentBodyRef}
registerScroller={this.setScrollerRef}
onScroll={this.onScroll}
>
{
@ -120,19 +115,17 @@ class ImportMovie extends Component {
}
{
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
<ImportMovieTableConnector
rootFolderId={rootFolderId}
unmappedFolders={unmappedFolders}
allSelected={allSelected}
allUnselected={allUnselected}
selectedState={selectedState}
contentBody={contentBody}
scrollTop={this.state.scrollTop}
scroller={scroller}
onSelectAllChange={this.onSelectAllChange}
onSelectedChange={this.onSelectedChange}
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
onScroll={this.onScroll}
/>
}
</PageContentBodyConnector>

@ -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 (
<VirtualTableRow style={style}>
<>
<VirtualTableSelectCell
inputClassName={styles.selectInput}
id={id}
@ -69,12 +67,11 @@ function ImportMovieRow(props) {
isExistingMovie={isExistingMovie}
/>
</VirtualTableRowCell>
</VirtualTableRow>
</>
);
}
ImportMovieRow.propTypes = {
style: PropTypes.object.isRequired,
id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired,
qualityProfileId: PropTypes.number.isRequired,

@ -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 (
<ImportMovieRowConnector
<VirtualTableRow
key={key}
style={style}
rootFolderId={rootFolderId}
isSelected={selectedState[item.id]}
onSelectedChange={onSelectedChange}
id={item.id}
/>
>
<ImportMovieRowConnector
key={item.id}
rootFolderId={rootFolderId}
isSelected={selectedState[item.id]}
onSelectedChange={onSelectedChange}
id={item.id}
/>
</VirtualTableRow>
);
}
@ -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 (
<VirtualTable
items={items}
contentBody={contentBody}
isSmallScreen={isSmallScreen}
scroller={scroller}
rowHeight={52}
scrollTop={scrollTop}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
header={
@ -155,7 +157,6 @@ class ImportMovieTable extends Component {
/>
}
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;

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

@ -12,3 +12,9 @@
flex-grow: 1;
width: 100%;
}
.errorMessage {
margin-top: 20px;
text-align: center;
font-size: 20px;
}

@ -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}
>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
{
isMeasured ?
<PageComponent
useCurrentPage={useCurrentPage}
/> :
<div />
}
</Measure>
{
movieError &&
<div className={styles.errorMessage}>
{getErrorMessage(movieError, 'Failed to load movie from API')}
</div>
}
{
!movieError &&
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
{
isMeasured ?
<PageComponent
useCurrentPage={useCurrentPage}
/> :
<div />
}
</Measure>
}
{
hasMovie &&
hasMovie && !! movieError &&
<LegendConnector />
}
</PageContentBodyConnector>
@ -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,

@ -72,7 +72,8 @@ function createMapStateToProps() {
selectedFilterKey,
filters,
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasMovie: !!movieCount,
hasMovie: !!movieCount.count,
movieError: movieCount.error,
missingMovieIds,
isSearchingForMissing
};

@ -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 (
<LoadingIndicator />
);
}
return (
<MovieSearchResult
{...item.item}
@ -128,7 +124,7 @@ class MovieSearchInput extends Component {
highlightedSuggestionIndex
} = this._autosuggest.state;
if (!suggestions.length || highlightedSectionIndex) {
if (!suggestions.length || suggestions[0].type === LOADING_TYPE || highlightedSectionIndex) {
this.props.onGoToAddNewMovie(value);
this._autosuggest.input.blur();
this.reset();
@ -154,35 +150,30 @@ class MovieSearchInput extends Component {
}
onSuggestionsFetchRequested = ({ value }) => {
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 = () => {

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

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

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

@ -1,5 +1,5 @@
.jumpBarItem {
flex: 1 0 $jumpBarItemHeight;
flex: 1 1 $jumpBarItemHeight;
border-bottom: 1px solid $borderColor;
text-align: center;
font-weight: bold;

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

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

@ -1,3 +1,7 @@
.tableContainer {
width: 100%;
}
.tableBodyContainer {
position: relative;
}

@ -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 (
<Measure onMeasure={this.onMeasure}>
<WindowScroller
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={this.onScroll}
>
{({ height, isScrolling }) => {
return (
<WindowScroller
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return null;
}
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<Scroller
className={className}
scrollDirection={scrollDirections.HORIZONTAL}
>
{header}
<VirtualTableBody
autoContainerWidth={true}
width={width}
height={height}
headerHeight={height - headerHeight}
rowHeight={ROW_HEIGHT}
rowCount={items.length}
columnCount={1}
scrollTop={scrollTop}
autoHeight={true}
overscanRowCount={2}
cellRenderer={rowRenderer}
columnWidth={width}
overscanIndicesGetter={overscanIndicesGetter}
onSectionRendered={this.onSectionRendered}
{...otherProps}
/>
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
autoContainerWidth={true}
autoHeight={true}
autoWidth={true}
width={width}
height={height}
headerHeight={height - headerHeight}
rowHeight={ROW_HEIGHT}
rowCount={items.length}
columnCount={1}
columnWidth={width}
scrollTop={scrollTop}
onScroll={onChildScroll}
overscanRowCount={2}
cellRenderer={rowRenderer}
overscanIndicesGetter={overscanIndicesGetter}
scrollToAlignment={'start'}
isScrollingOptout={true}
className={styles.tableBodyContainer}
style={gridStyle}
containerStyle={containerStyle}
{...otherProps}
/>
</div>
</Scroller>
);
}
}
</WindowScroller>
</Measure>
</Measure>
);
}
}
</WindowScroller>
);
}
}
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;

@ -1,3 +0,0 @@
.tableBodyContainer {
position: relative;
}

@ -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 (
<Grid
{...this.props}
style={{
boxSizing: undefined,
direction: undefined,
height: undefined,
position: undefined,
willChange: undefined,
overflow: undefined,
width: undefined
}}
containerStyle={{
position: undefined
}}
/>
);
}
}
VirtualTableBody.propTypes = {
className: PropTypes.string.isRequired
};
VirtualTableBody.defaultProps = {
className: styles.tableBodyContainer
};
export default VirtualTableBody;

@ -8,6 +8,12 @@
height: 350px;
}
.errorMessage {
margin-top: 20px;
text-align: center;
font-size: 20px;
}
.backdrop {
position: absolute;
z-index: -1;

@ -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 (
<PageContent title='loading'>
<PageContentBodyConnector>
<LoadingIndicator />
</PageContentBodyConnector>
</PageContent>
);
}
if (!isFetching && !!error) {
return (
<div className={styles.errorMessage}>
{getErrorMessage(error, 'Failed to load movie from API')}
</div>
);
}
if (!titleSlug) {
return (
<NotFound
@ -69,6 +107,9 @@ class MovieDetailsPageConnector extends Component {
MovieDetailsPageConnector.propTypes = {
titleSlug: PropTypes.string,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
match: PropTypes.shape({ params: PropTypes.shape({ titleSlug: PropTypes.string.isRequired }).isRequired }).isRequired,
push: PropTypes.func.isRequired
};

@ -4,6 +4,12 @@
overflow: hidden;
}
.errorMessage {
margin-top: 20px;
text-align: center;
font-size: 20px;
}
.contentBody {
composes: contentBody from '~Components/Page/PageContentBody.css';

@ -1,7 +1,8 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
@ -54,8 +55,8 @@ class MovieIndex extends Component {
super(props, context);
this.state = {
contentBody: null,
jumpBarItems: [],
scroller: null,
jumpBarItems: { order: [] },
jumpToCharacter: null,
isPosterOptionsModalOpen: false,
isOverviewOptionsModalOpen: false,
@ -67,8 +68,7 @@ class MovieIndex extends Component {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isRendered: false
selectedState: {}
};
}
@ -82,21 +82,19 @@ class MovieIndex extends Component {
items,
sortKey,
sortDirection,
scrollTop,
isDeleting,
deleteError
} = this.props;
if (
hasDifferentItems(prevProps.items, items) ||
sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection
if (sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection ||
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setJumpBarItems();
this.setSelectedState();
}
if (this.state.jumpToCharacter != null && scrollTop !== prevProps.scrollTop) {
if (this.state.jumpToCharacter != null) {
this.setState({ jumpToCharacter: null });
}
@ -112,11 +110,14 @@ class MovieIndex extends Component {
//
// Control
setContentBodyRef = (ref) => {
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 {
<div className={styles.pageContentBodyWrapper}>
<PageContentBodyConnector
ref={this.setContentBodyRef}
registerScroller={this.setScrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
scrollTop={isRendered ? scrollTop : 0}
onScroll={this.onScroll}
onScroll={onScroll}
>
{
isFetching && !isPopulated &&
@ -502,21 +491,21 @@ class MovieIndex extends Component {
{
!isFetching && !!error &&
<div>Unable to load movies</div>
<div className={styles.errorMessage}>
{getErrorMessage(error, 'Failed to load movie from API')}
</div>
}
{
isLoaded &&
<div className={styles.contentBodyContainer}>
<ViewComponent
contentBody={contentBody}
scroller={scroller}
items={items}
filters={filters}
sortKey={sortKey}
sortDirection={sortDirection}
scrollTop={scrollTop}
jumpToCharacter={jumpToCharacter}
onRender={this.onRender}
isMovieEditorActive={isMovieEditorActive}
allSelected={allSelected}
allUnselected={allUnselected}
@ -540,7 +529,7 @@ class MovieIndex extends Component {
</PageContentBodyConnector>
{
isLoaded && !!jumpBarItems.length &&
isLoaded && !!jumpBarItems.order.length &&
<PageJumpBar
items={jumpBarItems}
onItemPress={this.onJumpBarItemPress}
@ -624,7 +613,6 @@ MovieIndex.propTypes = {
isOrganizingMovie: PropTypes.bool.isRequired,
isSearchingMovies: PropTypes.bool.isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired,
scrollTop: PropTypes.number.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,

@ -3,7 +3,6 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
import dimensions from 'Styles/Variables/dimensions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
@ -14,29 +13,6 @@ import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import MovieIndex from './MovieIndex';
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(
createMovieClientSideCollectionItemsSelector('movieIndex'),
@ -115,23 +91,6 @@ function createMapDispatchToProps(dispatch, props) {
class MovieIndexConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
view,
scrollTop,
isSmallScreen
} = props;
this.state = {
scrollTop: getScrollTop(view, scrollTop, isSmallScreen)
};
}
componentDidMount() {
// TODO: Fetch root folders here for now, but should eventually fetch on editor toggle and check loaded before showing controls
this.props.dispatchFetchRootFolders();
@ -142,9 +101,7 @@ class MovieIndexConnector extends Component {
onViewSelect = (view) => {
// 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 (
<MovieIndex
{...this.props}
scrollTop={this.state.scrollTop}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
@ -178,7 +130,6 @@ class MovieIndexConnector extends Component {
MovieIndexConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
scrollTop: PropTypes.number.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchSetMovieView: PropTypes.func.isRequired,
dispatchSaveMovieEditor: PropTypes.func.isRequired

@ -1,13 +1,5 @@
$hoverScale: 1.05;
.container {
&:hover {
.content {
background-color: $tableRowHoverBackgroundColor;
}
}
}
.content {
display: flex;
flex-grow: 1;

@ -80,7 +80,6 @@ class MovieIndexOverview extends Component {
render() {
const {
style,
id,
title,
overview,
@ -126,7 +125,7 @@ class MovieIndexOverview extends Component {
const overviewHeight = contentHeight - titleRowHeight;
return (
<div className={styles.container} style={style}>
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.poster}>
<div className={styles.posterContainer}>
@ -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,

@ -1,3 +1,11 @@
.grid {
flex: 1 0 auto;
}
.container {
&:hover {
.content {
background-color: $tableRowHoverBackgroundColor;
}
}
}

@ -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 (
<MovieIndexItemConnector
<div
className={styles.container}
key={key}
component={MovieIndexOverview}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
rowHeight={rowHeight}
overviewOptions={overviewOptions}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
style={style}
movieId={movie.id}
qualityProfileId={movie.qualityProfileId}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/>
>
<MovieIndexItemConnector
key={movie.id}
component={MovieIndexOverview}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
rowHeight={rowHeight}
overviewOptions={overviewOptions}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
movieId={movie.id}
qualityProfileId={movie.qualityProfileId}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/>
</div>
);
}
@ -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 (
<Measure onMeasure={this.onMeasure}>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={onScroll}
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, isScrolling }) => {
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
onSectionRendered={this.onSectionRendered}
selectedState={selectedState}
/>
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
selectedState={selectedState}
scrollToAlignment={'start'}
isScrollingOptout={true}
/>
</div>
);
}
}
@ -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

@ -1,9 +1,5 @@
$hoverScale: 1.05;
.container {
padding: 10px;
}
.content {
transition: all 200ms ease-in;

@ -76,7 +76,6 @@ class MovieIndexPoster extends Component {
render() {
const {
style,
id,
title,
monitored,
@ -119,11 +118,10 @@ class MovieIndexPoster extends Component {
};
return (
<div className={styles.container} style={style}>
<div className={styles.content}>
<div className={styles.posterContainer}>
{
isMovieEditorActive &&
<div className={styles.content}>
<div className={styles.posterContainer}>
{
isMovieEditorActive &&
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
@ -132,126 +130,124 @@ class MovieIndexPoster extends Component {
onChange={this.onChange}
/>
</div>
}
<Label className={styles.controls}>
<SpinnerIconButton
className={styles.action}
name={icons.REFRESH}
title="Refresh movie"
isSpinning={isRefreshingMovie}
onPress={onRefreshMoviePress}
/>
{
showSearchAction &&
<SpinnerIconButton
className={styles.action}
name={icons.SEARCH}
title="Search for movie"
isSpinning={isSearchingMovie}
onPress={onSearchPress}
/>
}
<IconButton
className={styles.action}
name={icons.EDIT}
title="Edit movie"
onPress={this.onEditMoviePress}
/>
</Label>
}
<Label className={styles.controls}>
<SpinnerIconButton
className={styles.action}
name={icons.REFRESH}
title="Refresh movie"
isSpinning={isRefreshingMovie}
onPress={onRefreshMoviePress}
/>
{
status === 'ended' &&
<div
className={styles.ended}
title="Ended"
showSearchAction &&
<SpinnerIconButton
className={styles.action}
name={icons.SEARCH}
title="Search for movie"
isSpinning={isSearchingMovie}
onPress={onSearchPress}
/>
}
<Link
className={styles.link}
style={elementStyle}
to={link}
>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{title}
</div>
}
</Link>
</div>
<MovieIndexProgressBar
monitored={monitored}
hasFile={hasFile}
status={status}
posterWidth={posterWidth}
detailedProgressBar={detailedProgressBar}
/>
<IconButton
className={styles.action}
name={icons.EDIT}
title="Edit movie"
onPress={this.onEditMoviePress}
/>
</Label>
{
showTitle &&
<div className={styles.title}>
{title}
</div>
}
{
showMonitored &&
<div className={styles.title}>
{monitored ? 'Monitored' : 'Unmonitored'}
</div>
status === 'ended' &&
<div
className={styles.ended}
title="Ended"
/>
}
{
showQualityProfile &&
<div className={styles.title}>
{qualityProfile.name}
</div>
}
<Link
className={styles.link}
style={elementStyle}
to={link}
>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
<MovieIndexPosterInfo
qualityProfile={qualityProfile}
showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
{...otherProps}
/>
<EditMovieModalConnector
isOpen={isEditMovieModalOpen}
movieId={id}
onModalClose={this.onEditMovieModalClose}
onDeleteMoviePress={this.onDeleteMoviePress}
/>
<DeleteMovieModal
isOpen={isDeleteMovieModalOpen}
movieId={id}
onModalClose={this.onDeleteMovieModalClose}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{title}
</div>
}
</Link>
</div>
<MovieIndexProgressBar
monitored={monitored}
hasFile={hasFile}
status={status}
posterWidth={posterWidth}
detailedProgressBar={detailedProgressBar}
/>
{
showTitle &&
<div className={styles.title}>
{title}
</div>
}
{
showMonitored &&
<div className={styles.title}>
{monitored ? 'Monitored' : 'Unmonitored'}
</div>
}
{
showQualityProfile &&
<div className={styles.title}>
{qualityProfile.name}
</div>
}
<MovieIndexPosterInfo
qualityProfile={qualityProfile}
showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
{...otherProps}
/>
<EditMovieModalConnector
isOpen={isEditMovieModalOpen}
movieId={id}
onModalClose={this.onEditMovieModalClose}
onDeleteMoviePress={this.onDeleteMoviePress}
/>
<DeleteMovieModal
isOpen={isDeleteMovieModalOpen}
movieId={id}
onModalClose={this.onDeleteMovieModalClose}
/>
</div>
);
}
}
MovieIndexPoster.propTypes = {
style: PropTypes.object.isRequired,
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,

@ -1,3 +1,7 @@
.grid {
flex: 1 0 auto;
}
.container {
padding: 10px;
}

@ -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 (
<MovieIndexItemConnector
<div
className={styles.container}
key={key}
component={MovieIndexPoster}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
detailedProgressBar={detailedProgressBar}
showTitle={showTitle}
showMonitored={showMonitored}
showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
style={style}
movieId={movie.id}
qualityProfileId={movie.qualityProfileId}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/>
>
<MovieIndexItemConnector
key={movie.id}
component={MovieIndexPoster}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
detailedProgressBar={detailedProgressBar}
showTitle={showTitle}
showMonitored={showMonitored}
showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
movieId={movie.id}
qualityProfileId={movie.qualityProfileId}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/>
</div>
);
}
@ -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 (
<Measure onMeasure={this.onMeasure}>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={onScroll}
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, isScrolling }) => {
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
onSectionRendered={this.onSectionRendered}
selectedState={selectedState}
/>
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
selectedState={selectedState}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
@ -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

@ -79,6 +79,7 @@
composes: cell;
flex: 0 1 90px;
min-width: 60px;
}
.checkInput {

@ -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 (
<VirtualTableRow style={style}>
<>
{
columns.map((column) => {
const {
@ -339,13 +334,12 @@ class MovieIndexRow extends Component {
movieId={id}
onModalClose={this.onDeleteMovieModalClose}
/>
</VirtualTableRow>
</>
);
}
}
MovieIndexRow.propTypes = {
style: PropTypes.object.isRequired,
id: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,

@ -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 (
<MovieIndexItemConnector
<VirtualTableRow
key={key}
component={MovieIndexRow}
style={style}
columns={columns}
movieId={movie.id}
qualityProfileId={movie.qualityProfileId}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/>
>
<MovieIndexItemConnector
key={movie.id}
component={MovieIndexRow}
columns={columns}
movieId={movie.id}
qualityProfileId={movie.qualityProfileId}
isSelected={selectedState[movie.id]}
onSelectedChange={onSelectedChange}
isMovieEditorActive={isMovieEditorActive}
/>
</VirtualTableRow>
);
}
@ -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 {
<VirtualTable
className={styles.tableContainer}
items={items}
scrollTop={scrollTop}
scrollIndex={this.state.scrollIndex}
contentBody={contentBody}
isSmallScreen={isSmallScreen}
scroller={scroller}
rowHeight={38}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
@ -117,11 +116,6 @@ class MovieIndexTable extends Component {
}
selectedState={selectedState}
columns={columns}
filters={filters}
sortKey={sortKey}
sortDirection={sortDirection}
onRender={onRender}
onScroll={onScroll}
/>
);
}
@ -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,

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

@ -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) => {

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

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

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

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

@ -47,8 +47,8 @@
content="/Content/Images/Icons/browserconfig.xml"
/>
<link rel="stylesheet" type="text/css" href="/Content/styles.css" />
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css" />
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
<!-- webpack bundles head -->
<title>Radarr (Preview)</title>
@ -80,7 +80,5 @@
<script src="/initialize.js" data-no-hash></script>
<script src="/polyfills.js"></script>
<script src="/vendor.js"></script>
<script src="/preload.js"></script>
<script src="/index.js"></script>
<!-- webpack bundles body -->
</html>

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

@ -1,2 +0,0 @@
/* eslint no-undef: 0 */
__webpack_public_path__ = `${window.Radarr.urlBase}/`;

@ -1,5 +0,0 @@
/* Base */
// require('jquery');
require('lodash');
require('moment');
// require('signalR');

@ -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": [

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

@ -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<Movie> 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<Movie, AlternativeTitle>(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<AlternativeTitle>(t => t.CleanTitle == cleanTitle || t.CleanTitle == cleanTitleWithArabicNumbers || t.CleanTitle == cleanTitleWithRomanNumbers)
.FirstWithYear(year);
}
}
return result;
}
public List<Movie> FindByTitleInexact(string cleanTitle)
{
var mapper = _database.GetDataMapper();
mapper.AddParameter("queryTitle", cleanTitle);
return AddJoinQueries(mapper.Query<Movie>()).Where($"instr(@queryTitle, [t0].[CleanTitle])");
}
public Movie FindByTmdbId(int tmdbid)
{
return Query.Where(m => m.TmdbId == tmdbid).FirstOrDefault();

@ -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<Movie> AddMovies(List<Movie> 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<Movie> 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;
}

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

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

Loading…
Cancel
Save