1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00
Files
Radarr/frontend/src/DiscoverMovie/Posters/DiscoverMoviePosters.js
T
Bogdan 159f5df8cc Fix jump to character for Collections and Discover
Fix for a regression introduced in react-virtualized 9.21.2 when WindowScroller is used with Grids
2025-05-22 15:40:29 +03:00

372 lines
9.3 KiB
JavaScript

import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import DiscoverMovieItemConnector from 'DiscoverMovie/DiscoverMovieItemConnector';
import dimensions from 'Styles/Variables/dimensions';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import DiscoverMoviePosterConnector from './DiscoverMoviePosterConnector';
import styles from './DiscoverMoviePosters.css';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
const additionalColumnCount = {
small: 3,
medium: 2,
large: 1
};
function calculateColumnWidth(width, posterSize, isSmallScreen) {
const maximumColumnWidth = isSmallScreen ? 172 : 182;
const columns = Math.floor(width / maximumColumnWidth);
const remainder = width % maximumColumnWidth;
if (remainder === 0 && posterSize === 'large') {
return maximumColumnWidth;
}
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
}
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) {
const {
detailedProgressBar,
showTitle,
showTmdbRating,
showImdbRating,
showRottenTomatoesRating,
showTraktRating
} = posterOptions;
const heights = [
posterHeight,
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
isSmallScreen ? columnPaddingSmallScreen : columnPadding
];
if (showTitle) {
heights.push(19);
}
if (showTmdbRating) {
heights.push(19);
}
if (showImdbRating) {
heights.push(19);
}
if (showRottenTomatoesRating) {
heights.push(19);
}
if (showTraktRating) {
heights.push(19);
}
switch (sortKey) {
case 'studio':
case 'inCinemas':
case 'digitalRelease':
case 'physicalRelease':
case 'runtime':
case 'certification':
heights.push(19);
break;
case 'tmdbRating':
if (!showTmdbRating) {
heights.push(19);
}
break;
case 'imdbRating':
if (!showImdbRating) {
heights.push(19);
}
break;
case 'rottenTomatoesRating':
if (!showRottenTomatoesRating) {
heights.push(19);
}
break;
case 'traktRating':
if (!showTraktRating) {
heights.push(19);
}
break;
default:
// No need to add a height of 0
}
return heights.reduce((acc, height) => acc + height, 0);
}
function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth);
}
class DiscoverMoviePosters extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
width: 0,
columnWidth: 182,
columnCount: 1,
posterWidth: 162,
posterHeight: 238,
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
};
this._isInitialized = false;
this._grid = null;
this._padding = props.isSmallScreen ? columnPaddingSmallScreen : columnPadding;
}
componentDidUpdate(prevProps, prevState) {
const {
items,
sortKey,
posterOptions,
jumpToCharacter,
isSmallScreen,
selectedState
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
if (prevProps.sortKey !== sortKey ||
prevProps.posterOptions !== posterOptions) {
this.calculateGrid(width, isSmallScreen);
}
if (this._grid &&
(prevState.width !== width ||
prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight ||
prevProps.selectedState !== selectedState ||
hasDifferentItemsOrOrder(prevProps.items, items, 'tmdbId'))) {
// 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 (this._grid && index != null) {
const row = Math.floor(index / columnCount);
this._gridScrollToCell({
rowIndex: row,
columnIndex: 0
});
}
}
}
//
// Control
setGridRef = (ref) => {
this._grid = ref;
};
calculateGrid = (width = this.state.width, isSmallScreen) => {
const {
sortKey,
posterOptions
} = this.props;
const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen);
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
const posterWidth = columnWidth - this._padding * 2;
const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions);
this.setState({
width,
columnWidth,
columnCount,
posterWidth,
posterHeight,
rowHeight
});
};
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
const {
items,
sortKey,
posterOptions,
showRelativeDates,
shortDateFormat,
timeFormat,
selectedState,
onSelectedChange
} = this.props;
const {
posterWidth,
posterHeight,
columnCount
} = this.state;
const {
showTitle,
showTmdbRating,
showImdbRating,
showRottenTomatoesRating,
showTraktRating
} = posterOptions;
const movieIdx = rowIndex * columnCount + columnIndex;
const movie = items[movieIdx];
if (!movie) {
return null;
}
return (
<div
className={styles.container}
key={key}
style={{
...style,
padding: this._padding
}}
>
<DiscoverMovieItemConnector
key={movie.tmdbId}
component={DiscoverMoviePosterConnector}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
showTitle={showTitle}
showTmdbRating={showTmdbRating}
showImdbRating={showImdbRating}
showRottenTomatoesRating={showRottenTomatoesRating}
showTraktRating={showTraktRating}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
movieId={movie.tmdbId}
isSelected={selectedState[movie.tmdbId]}
onSelectedChange={onSelectedChange}
/>
</div>
);
};
_gridScrollToCell = ({ rowIndex = 0, columnIndex = 0 }) => {
const scrollOffset = this._grid.getOffsetForCell({
rowIndex,
columnIndex
});
this._gridScrollToPosition(scrollOffset);
};
_gridScrollToPosition = ({ scrollTop = 0, scrollLeft = 0 }) => {
this.props.scroller?.scrollTo({ top: scrollTop, left: scrollLeft });
};
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
};
//
// Render
render() {
const {
isSmallScreen,
scroller,
items,
selectedState
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
const rowCount = Math.ceil(items.length / columnCount);
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<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>
);
}
}
</WindowScroller>
</Measure>
);
}
}
DiscoverMoviePosters.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
posterOptions: PropTypes.object.isRequired,
jumpToCharacter: PropTypes.string,
scroller: PropTypes.instanceOf(Element).isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired
};
export default DiscoverMoviePosters;