You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Radarr/frontend/src/Movie/Index/Posters/MovieIndexPosters.tsx

347 lines
8.2 KiB

import { throttle } from 'lodash';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import useMeasure from 'Helpers/Hooks/useMeasure';
import SortDirection from 'Helpers/Props/SortDirection';
import MovieIndexPoster from 'Movie/Index/Posters/MovieIndexPoster';
import Movie from 'Movie/Movie';
import dimensions from 'Styles/Variables/dimensions';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
const bodyPaddingSmallScreen = parseInt(
dimensions.pageContentBodyPaddingSmallScreen
);
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(
dimensions.movieIndexColumnPaddingSmallScreen
);
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
small: 3,
medium: 2,
large: 1,
};
interface CellItemData {
layout: {
columnCount: number;
padding: number;
posterWidth: number;
posterHeight: number;
};
items: Movie[];
sortKey: string;
isSelectMode: boolean;
}
interface MovieIndexPostersProps {
items: Movie[];
sortKey: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const movieIndexSelector = createSelector(
(state: AppState) => state.movieIndex.posterOptions,
(posterOptions) => {
return {
posterOptions,
};
}
);
const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
columnIndex,
rowIndex,
style,
data,
}) => {
const { layout, items, sortKey, isSelectMode } = data;
const { columnCount, padding, posterWidth, posterHeight } = layout;
const index = rowIndex * columnCount + columnIndex;
if (index >= items.length) {
return null;
}
const movie = items[index];
return (
<div
style={{
padding,
...style,
}}
>
<MovieIndexPoster
movieId={movie.id}
sortKey={sortKey}
isSelectMode={isSelectMode}
posterWidth={posterWidth}
posterHeight={posterHeight}
/>
</div>
);
};
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;
}
export default function MovieIndexPosters(props: MovieIndexPostersProps) {
const {
scrollerRef,
items,
sortKey,
jumpToCharacter,
isSelectMode,
isSmallScreen,
} = props;
const { posterOptions } = useSelector(movieIndexSelector);
const ref = useRef<Grid>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
const columnWidth = useMemo(() => {
const { width } = size;
const maximumColumnWidth = isSmallScreen ? 172 : 182;
const columns = Math.floor(width / maximumColumnWidth);
const remainder = width % maximumColumnWidth;
return remainder === 0
? maximumColumnWidth
: Math.floor(
width / (columns + ADDITIONAL_COLUMN_COUNT[posterOptions.size])
);
}, [isSmallScreen, posterOptions, size]);
const columnCount = useMemo(
() => Math.max(Math.floor(size.width / columnWidth), 1),
[size, columnWidth]
);
const padding = props.isSmallScreen
? columnPaddingSmallScreen
: columnPadding;
const posterWidth = columnWidth - padding * 2;
const posterHeight = Math.ceil((250 / 170) * posterWidth);
const rowHeight = useMemo(() => {
const {
detailedProgressBar,
showTitle,
showMonitored,
showQualityProfile,
showCinemaRelease,
showReleaseDate,
showTmdbRating,
showImdbRating,
showRottenTomatoesRating,
} = posterOptions;
const nextAiringHeight = 19;
const heights = [
posterHeight,
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
nextAiringHeight,
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
];
if (showTitle) {
heights.push(19);
}
if (showMonitored) {
heights.push(19);
}
if (showQualityProfile) {
heights.push(19);
}
if (showCinemaRelease) {
heights.push(19);
}
if (showReleaseDate) {
heights.push(19);
}
if (showTmdbRating) {
heights.push(19);
}
if (showImdbRating) {
heights.push(19);
}
if (showRottenTomatoesRating) {
heights.push(19);
}
switch (sortKey) {
case 'studio':
case 'added':
case 'year':
case 'path':
case 'sizeOnDisk':
case 'originalTitle':
case 'originalLanguage':
heights.push(19);
break;
case 'qualityProfileId':
if (!showQualityProfile) {
heights.push(19);
}
break;
case 'inCinemas':
if (!showCinemaRelease) {
heights.push(19);
}
break;
case 'digitalRelease':
case 'physicalRelease':
if (!showReleaseDate) {
heights.push(19);
}
break;
case 'imdbRating':
if (!showImdbRating) {
heights.push(19);
}
break;
case 'tmdbRating':
if (!showTmdbRating) {
heights.push(19);
}
break;
case 'rottenTomatoesRating':
if (!showRottenTomatoesRating) {
heights.push(19);
}
break;
default:
// No need to add a height of 0
}
return heights.reduce((acc, height) => acc + height, 0);
}, [isSmallScreen, posterOptions, sortKey, posterHeight]);
useEffect(() => {
const current = scrollerRef.current;
if (isSmallScreen) {
const padding = bodyPaddingSmallScreen - 5;
setSize({
width: window.innerWidth - padding * 2,
height: window.innerHeight,
});
return;
}
if (current) {
const width = current.clientWidth;
const padding = bodyPadding - 5;
setSize({
width: width - padding * 2,
height: window.innerHeight,
});
}
}, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => {
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
const scrollTop =
(isSmallScreen
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
return () => {
handleScroll.cancel();
if (currentScrollListener) {
currentScrollListener.removeEventListener('scroll', handleScroll);
}
};
}, [isSmallScreen, ref, scrollerRef]);
useEffect(() => {
if (jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (index != null) {
const rowIndex = Math.floor(index / columnCount);
const scrollTop = rowIndex * rowHeight + padding;
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current?.scrollTo(0, scrollTop);
}
}
}, [
jumpToCharacter,
rowHeight,
columnCount,
padding,
items,
scrollerRef,
ref,
]);
return (
<div ref={measureRef}>
<Grid<CellItemData>
ref={ref}
style={{
width: '100%',
height: '100%',
overflow: 'none',
}}
width={size.width}
height={size.height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={Math.ceil(items.length / columnCount)}
rowHeight={rowHeight}
itemData={{
layout: {
columnCount,
padding,
posterWidth,
posterHeight,
},
items,
sortKey,
isSelectMode,
}}
>
{Cell}
</Grid>
</div>
);
}