mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-04-24 05:47:22 -04:00
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
This commit is contained in:
parent
95e5e3132b
commit
abe7a85a39
65 changed files with 1529 additions and 1305 deletions
|
@ -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;
|
||||
if (this._grid && index != null) {
|
||||
|
||||
const scrollTop = rowHeight * index;
|
||||
|
||||
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]}
|
||||
>
|
||||
{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}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'certification') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{certification}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
<>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
<AddNewMovieModal
|
||||
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
|
||||
tmdbId={tmdbId}
|
||||
title={title}
|
||||
year={year}
|
||||
overview={overview}
|
||||
folder={folder}
|
||||
images={images}
|
||||
onModalClose={this.onAddMovieModalClose}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
</div>
|
||||
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}
|
||||
>
|
||||
{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}
|
||||
/>
|
||||
</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>
|
||||
}
|
||||
|
||||
{
|
||||
hasMovie &&
|
||||
!movieError &&
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
{
|
||||
isMeasured ?
|
||||
<PageComponent
|
||||
useCurrentPage={useCurrentPage}
|
||||
/> :
|
||||
<div />
|
||||
}
|
||||
</Measure>
|
||||
}
|
||||
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
this.setState({ suggestions });
|
||||
workerInstance.postMessage(payload);
|
||||
}, 250);
|
||||
|
||||
onSuggestionsReceived = (message) => {
|
||||
this.setState({
|
||||
suggestions: message.data
|
||||
});
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
|
|
63
frontend/src/Components/Page/Header/fuse.worker.js
Normal file
63
frontend/src/Components/Page/Header/fuse.worker.js
Normal file
|
@ -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 visibleItems = _.reduce(items, (acc, item, index) => {
|
||||
if (index % removeDiff === 0) {
|
||||
acc.push(item);
|
||||
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;
|
||||
|
||||
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 (this._grid &&
|
||||
(prevState.width !== width ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
||||
const scrollTop = (scrollIndex + 1) * ROW_HEIGHT + 20;
|
||||
|
||||
this.props.onScroll({ scrollTop });
|
||||
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(firstCharacter)) {
|
||||
acc.push(firstCharacter);
|
||||
if (!isNaN(char)) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
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}
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title="Edit movie"
|
||||
onPress={this.onEditMoviePress}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
className={styles.ended}
|
||||
title="Ended"
|
||||
/>
|
||||
|
||||
{
|
||||
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>
|
||||
}
|
||||
<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}
|
||||
/>
|
||||
|
||||
{
|
||||
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}
|
||||
/>
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
return diff1.length > 0 || diff2.length > 0;
|
||||
if (prevItems.length !== currentItems.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentItemIds = new Set();
|
||||
|
||||
currentItems.forEach((currentItem) => {
|
||||
currentItemIds.add(currentItem[idProp]);
|
||||
});
|
||||
|
||||
return prevItems.every((prevItem) => currentItemIds.has(prevItem[idProp]));
|
||||
}
|
||||
|
||||
export default hasDifferentItems;
|
||||
|
|
21
frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js
Normal file
21
frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
261
yarn.lock
261
yarn.lock
|
@ -1838,6 +1838,11 @@ beeper@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809"
|
||||
integrity sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=
|
||||
|
||||
big.js@^3.1.3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
|
||||
integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
|
||||
|
||||
big.js@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
|
@ -1883,6 +1888,11 @@ body@^5.1.0:
|
|||
raw-body "~1.1.0"
|
||||
safe-json-parse "~1.0.1"
|
||||
|
||||
boolbase@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
|
@ -2109,6 +2119,14 @@ callsites@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
|
||||
|
||||
camel-case@3.0.x:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
|
||||
integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=
|
||||
dependencies:
|
||||
no-case "^2.2.0"
|
||||
upper-case "^1.1.1"
|
||||
|
||||
camelcase-css@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
|
||||
|
@ -2252,6 +2270,13 @@ classnames@2.2.6, classnames@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||
|
||||
clean-css@4.2.x:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
|
||||
integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==
|
||||
dependencies:
|
||||
source-map "~0.6.0"
|
||||
|
||||
cli-cursor@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
|
||||
|
@ -2400,11 +2425,21 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
|
|||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@2.17.x:
|
||||
version "2.17.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
|
||||
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
|
||||
|
||||
commander@^2.2.0, commander@^2.20.0:
|
||||
version "2.20.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
|
||||
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
|
||||
|
||||
commander@~2.19.0:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
|
||||
integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
|
@ -2658,6 +2693,21 @@ css-loader@3.0.0:
|
|||
postcss-value-parser "^4.0.0"
|
||||
schema-utils "^1.0.0"
|
||||
|
||||
css-select@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
|
||||
integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
|
||||
dependencies:
|
||||
boolbase "~1.0.0"
|
||||
css-what "2.1"
|
||||
domutils "1.5.1"
|
||||
nth-check "~1.0.1"
|
||||
|
||||
css-what@2.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
|
||||
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
|
||||
|
||||
css@2.X, css@^2.2.1:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
|
||||
|
@ -2965,6 +3015,13 @@ doctrine@^3.0.0:
|
|||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dom-converter@^0.2:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
|
||||
integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==
|
||||
dependencies:
|
||||
utila "~0.4"
|
||||
|
||||
dom-css@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-css/-/dom-css-2.1.0.tgz#fdbc2d5a015d0a3e1872e11472bbd0e7b9e6a202"
|
||||
|
@ -3011,6 +3068,14 @@ domhandler@^2.3.0:
|
|||
dependencies:
|
||||
domelementtype "1"
|
||||
|
||||
domutils@1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
|
||||
integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
|
||||
dependencies:
|
||||
dom-serializer "0"
|
||||
domelementtype "1"
|
||||
|
||||
domutils@^1.5.1:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
|
||||
|
@ -3169,6 +3234,22 @@ es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.7.0:
|
|||
is-regex "^1.0.4"
|
||||
object-keys "^1.0.12"
|
||||
|
||||
es-abstract@^1.5.1:
|
||||
version "1.16.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d"
|
||||
integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.0"
|
||||
function-bind "^1.1.1"
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.0"
|
||||
is-callable "^1.1.4"
|
||||
is-regex "^1.0.4"
|
||||
object-inspect "^1.6.0"
|
||||
object-keys "^1.1.1"
|
||||
string.prototype.trimleft "^2.1.0"
|
||||
string.prototype.trimright "^2.1.0"
|
||||
|
||||
es-to-primitive@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
|
||||
|
@ -4482,6 +4563,11 @@ hash.js@^1.0.0, hash.js@^1.0.3:
|
|||
inherits "^2.0.3"
|
||||
minimalistic-assert "^1.0.1"
|
||||
|
||||
he@1.2.x:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
history@4.9.0, history@^4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca"
|
||||
|
@ -4522,12 +4608,38 @@ hosted-git-info@^2.1.4:
|
|||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546"
|
||||
integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==
|
||||
|
||||
html-minifier@^3.2.3:
|
||||
version "3.5.21"
|
||||
resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c"
|
||||
integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==
|
||||
dependencies:
|
||||
camel-case "3.0.x"
|
||||
clean-css "4.2.x"
|
||||
commander "2.17.x"
|
||||
he "1.2.x"
|
||||
param-case "2.1.x"
|
||||
relateurl "0.2.x"
|
||||
uglify-js "3.4.x"
|
||||
|
||||
html-tags@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
|
||||
integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
|
||||
|
||||
htmlparser2@^3.10.0:
|
||||
html-webpack-plugin@3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
|
||||
integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s=
|
||||
dependencies:
|
||||
html-minifier "^3.2.3"
|
||||
loader-utils "^0.2.16"
|
||||
lodash "^4.17.3"
|
||||
pretty-error "^2.0.2"
|
||||
tapable "^1.0.0"
|
||||
toposort "^1.0.0"
|
||||
util.promisify "1.0.0"
|
||||
|
||||
htmlparser2@^3.10.0, htmlparser2@^3.3.0:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
|
||||
integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
|
||||
|
@ -5195,6 +5307,11 @@ json-stringify-safe@~5.0.1:
|
|||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
||||
|
||||
json5@^0.5.0:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
|
||||
integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
|
||||
|
||||
json5@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
|
||||
|
@ -5378,7 +5495,17 @@ loader-runner@^2.3.0, loader-runner@^2.4.0:
|
|||
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
|
||||
integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
|
||||
|
||||
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.2, loader-utils@^1.2.3:
|
||||
loader-utils@^0.2.16:
|
||||
version "0.2.17"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
|
||||
integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=
|
||||
dependencies:
|
||||
big.js "^3.1.3"
|
||||
emojis-list "^2.0.0"
|
||||
json5 "^0.5.0"
|
||||
object-assign "^4.0.1"
|
||||
|
||||
loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.2, loader-utils@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
|
||||
integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
|
||||
|
@ -5547,7 +5674,7 @@ lodash@4.17.14:
|
|||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
|
||||
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
|
||||
|
||||
lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.4:
|
||||
lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.3, lodash@^4.17.4:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
@ -5593,6 +5720,11 @@ loud-rejection@^1.0.0:
|
|||
currently-unhandled "^0.4.1"
|
||||
signal-exit "^3.0.0"
|
||||
|
||||
lower-case@^1.1.1:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac"
|
||||
integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw=
|
||||
|
||||
lru-cache@^4.0.1:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
|
||||
|
@ -6077,6 +6209,13 @@ nice-try@^1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||
|
||||
no-case@^2.2.0:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac"
|
||||
integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==
|
||||
dependencies:
|
||||
lower-case "^1.1.1"
|
||||
|
||||
node-fetch@^1.0.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
|
@ -6274,6 +6413,13 @@ nsdeclare@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/nsdeclare/-/nsdeclare-0.1.0.tgz#10daa153642382d3cf2c01a916f4eb20a128b19f"
|
||||
integrity sha1-ENqhU2QjgtPPLAGpFvTrIKEosZ8=
|
||||
|
||||
nth-check@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
|
||||
integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
|
||||
dependencies:
|
||||
boolbase "~1.0.0"
|
||||
|
||||
num2fraction@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
|
||||
|
@ -6308,7 +6454,12 @@ object-copy@^0.1.0:
|
|||
define-property "^0.2.5"
|
||||
kind-of "^3.0.3"
|
||||
|
||||
object-keys@^1.0.11, object-keys@^1.0.12:
|
||||
object-inspect@^1.6.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
|
||||
integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
|
||||
|
||||
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
|
||||
|
@ -6365,6 +6516,14 @@ object.fromentries@^2.0.0:
|
|||
function-bind "^1.1.1"
|
||||
has "^1.0.1"
|
||||
|
||||
object.getownpropertydescriptors@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
|
||||
integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=
|
||||
dependencies:
|
||||
define-properties "^1.1.2"
|
||||
es-abstract "^1.5.1"
|
||||
|
||||
object.map@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37"
|
||||
|
@ -6547,6 +6706,13 @@ parallel-transform@^1.1.0:
|
|||
inherits "^2.0.3"
|
||||
readable-stream "^2.1.5"
|
||||
|
||||
param-case@2.1.x:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
|
||||
integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc=
|
||||
dependencies:
|
||||
no-case "^2.2.0"
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
|
@ -7079,6 +7245,14 @@ preserve@^0.2.0:
|
|||
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
|
||||
integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=
|
||||
|
||||
pretty-error@^2.0.2:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
|
||||
integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=
|
||||
dependencies:
|
||||
renderkid "^2.0.1"
|
||||
utila "~0.4"
|
||||
|
||||
pretty-hrtime@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
|
||||
|
@ -7744,6 +7918,11 @@ regjsparser@^0.6.0:
|
|||
dependencies:
|
||||
jsesc "~0.5.0"
|
||||
|
||||
relateurl@0.2.x:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
||||
|
||||
remark-parse@^6.0.0:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-6.0.3.tgz#c99131052809da482108413f87b0ee7f52180a3a"
|
||||
|
@ -7816,6 +7995,17 @@ remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
||||
integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
|
||||
|
||||
renderkid@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149"
|
||||
integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==
|
||||
dependencies:
|
||||
css-select "^1.1.0"
|
||||
dom-converter "^0.2"
|
||||
htmlparser2 "^3.3.0"
|
||||
strip-ansi "^3.0.0"
|
||||
utila "^0.4.0"
|
||||
|
||||
repeat-element@^1.1.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
|
||||
|
@ -8125,6 +8315,14 @@ scheduler@^0.13.6:
|
|||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
schema-utils@^0.4.0:
|
||||
version "0.4.7"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"
|
||||
integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==
|
||||
dependencies:
|
||||
ajv "^6.1.0"
|
||||
ajv-keywords "^3.1.0"
|
||||
|
||||
schema-utils@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
|
||||
|
@ -8535,6 +8733,22 @@ string-width@^4.1.0:
|
|||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^5.2.0"
|
||||
|
||||
string.prototype.trimleft@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634"
|
||||
integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
function-bind "^1.1.1"
|
||||
|
||||
string.prototype.trimright@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
|
||||
integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
function-bind "^1.1.1"
|
||||
|
||||
string_decoder@0.10, string_decoder@~0.10.x:
|
||||
version "0.10.31"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
|
||||
|
@ -9009,6 +9223,11 @@ to-through@^2.0.0:
|
|||
dependencies:
|
||||
through2 "^2.0.3"
|
||||
|
||||
toposort@^1.0.0:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
|
||||
integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk=
|
||||
|
||||
tough-cookie@~2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
|
||||
|
@ -9101,6 +9320,14 @@ ua-parser-js@^0.7.18:
|
|||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098"
|
||||
integrity sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw==
|
||||
|
||||
uglify-js@3.4.x:
|
||||
version "3.4.10"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
|
||||
integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==
|
||||
dependencies:
|
||||
commander "~2.19.0"
|
||||
source-map "~0.6.1"
|
||||
|
||||
unc-path-regex@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
|
||||
|
@ -9259,6 +9486,11 @@ upath@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068"
|
||||
integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==
|
||||
|
||||
upper-case@^1.1.1:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598"
|
||||
integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
|
||||
|
@ -9313,6 +9545,14 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
util.promisify@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
|
||||
integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
|
||||
dependencies:
|
||||
define-properties "^1.1.2"
|
||||
object.getownpropertydescriptors "^2.0.3"
|
||||
|
||||
util@0.10.3:
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
|
||||
|
@ -9327,6 +9567,11 @@ util@^0.11.0:
|
|||
dependencies:
|
||||
inherits "2.0.3"
|
||||
|
||||
utila@^0.4.0, utila@~0.4:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
|
||||
integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
|
||||
|
||||
uuid@^3.3.2:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
|
||||
|
@ -9666,6 +9911,14 @@ worker-farm@^1.3.1, worker-farm@^1.7.0:
|
|||
dependencies:
|
||||
errno "~0.1.7"
|
||||
|
||||
worker-loader@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac"
|
||||
integrity sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==
|
||||
dependencies:
|
||||
loader-utils "^1.0.0"
|
||||
schema-utils "^0.4.0"
|
||||
|
||||
wrap-ansi@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue