Feature/automatically load more pages until scrollbar is visible (#667)

* Rename "grifRed" to "gridWrapperRef"

* Automatically fetch more pages until scrollbar is visible
This commit is contained in:
schroda
2024-03-23 23:34:33 +01:00
committed by GitHub
parent 83635faf95
commit 66d6c95dfd
2 changed files with 168 additions and 124 deletions

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import React, { ForwardedRef, forwardRef, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import Grid, { GridTypeMap } from '@mui/material/Grid'; import Grid, { GridTypeMap } from '@mui/material/Grid';
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso'; import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso';
@@ -73,7 +73,9 @@ type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection']; handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
}; };
const HorizontalGrid = ({ const HorizontalGrid = forwardRef(
(
{
isLoading, isLoading,
mangas, mangas,
inLibraryIndicator, inLibraryIndicator,
@@ -83,8 +85,11 @@ const HorizontalGrid = ({
selectedMangaIds, selectedMangaIds,
handleSelection, handleSelection,
mode, mode,
}: DefaultGridProps) => ( }: DefaultGridProps,
ref: ForwardedRef<HTMLDivElement | null>,
) => (
<Grid <Grid
ref={ref}
container container
spacing={1} spacing={1}
style={{ style={{
@@ -114,9 +119,12 @@ const HorizontalGrid = ({
)) ))
)} )}
</Grid> </Grid>
),
); );
const VerticalGrid = ({ const VerticalGrid = forwardRef(
(
{
isLoading, isLoading,
mangas, mangas,
inLibraryIndicator, inLibraryIndicator,
@@ -131,7 +139,9 @@ const VerticalGrid = ({
}: DefaultGridProps & { }: DefaultGridProps & {
hasNextPage: boolean; hasNextPage: boolean;
loadMore: () => void; loadMore: () => void;
}) => { },
ref: ForwardedRef<HTMLDivElement | null>,
) => {
const location = useLocation<{ snapshot?: GridStateSnapshot }>(); const location = useLocation<{ snapshot?: GridStateSnapshot }>();
const navigate = useNavigate(); const navigate = useNavigate();
const { snapshot } = location.state ?? {}; const { snapshot } = location.state ?? {};
@@ -157,6 +167,7 @@ const VerticalGrid = ({
return ( return (
<> <>
<Box ref={ref}>
<VirtuosoGrid <VirtuosoGrid
useWindowScroll useWindowScroll
overscan={window.innerHeight * 0.25} overscan={window.innerHeight * 0.25}
@@ -180,6 +191,7 @@ const VerticalGrid = ({
) )
} }
/> />
</Box>
{/* render div to prevent UI jumping around when showing/hiding loading placeholder */ {/* render div to prevent UI jumping around when showing/hiding loading placeholder */
/* eslint-disable-next-line no-nested-ternary */} /* eslint-disable-next-line no-nested-ternary */}
{isSelectModeActive && gridLayout === GridLayout.List ? ( {isSelectModeActive && gridLayout === GridLayout.List ? (
@@ -192,7 +204,8 @@ const VerticalGrid = ({
) : null} ) : null}
</> </>
); );
}; },
);
export interface IMangaGridProps extends Omit<DefaultGridProps, 'GridItemContainer'> { export interface IMangaGridProps extends Omit<DefaultGridProps, 'GridItemContainer'> {
message?: string; message?: string;
@@ -221,9 +234,11 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
mode, mode,
} = props; } = props;
const gridRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState(document.documentElement.offsetWidth); const [dimensions, setDimensions] = useState(document.documentElement.offsetWidth);
const [gridItemWidth] = useLocalStorage<number>('ItemWidth', 300); const [gridItemWidth] = useLocalStorage<number>('ItemWidth', 300);
const gridRef = useRef<HTMLDivElement>(null); const gridWrapperRef = useRef<HTMLDivElement>(null);
const GridItemContainer = useMemo( const GridItemContainer = useMemo(
() => GridItemContainerWithDimension(dimensions, gridItemWidth, gridLayout), () => GridItemContainerWithDimension(dimensions, gridItemWidth, gridLayout),
[dimensions, gridItemWidth, gridLayout], [dimensions, gridItemWidth, gridLayout],
@@ -231,7 +246,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
const updateGridWidth = () => { const updateGridWidth = () => {
const getDimensions = () => { const getDimensions = () => {
const gridWidth = gridRef.current?.offsetWidth; const gridWidth = gridWrapperRef.current?.offsetWidth;
if (!gridWidth) { if (!gridWidth) {
return document.documentElement.offsetWidth; return document.documentElement.offsetWidth;
@@ -258,6 +273,36 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
return () => window.removeEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize);
}, []); }, []);
useEffect(() => {
if (!gridRef.current) {
return () => {};
}
if (gridRef.current.offsetHeight > document.documentElement.clientHeight) {
return () => {};
}
const resizeObserver = new ResizeObserver(() => {
const gridHeight = gridRef.current!.offsetHeight;
const isScrollbarVisible = gridHeight > document.documentElement.clientHeight;
if (!gridHeight) {
return;
}
if (isScrollbarVisible) {
resizeObserver.disconnect();
return;
}
loadMore();
resizeObserver.disconnect();
});
resizeObserver.observe(gridRef.current);
return () => resizeObserver.disconnect();
}, [loadMore]);
const hasNoItems = !isLoading && mangas.length === 0; const hasNoItems = !isLoading && mangas.length === 0;
if (hasNoItems) { if (hasNoItems) {
if (noFaces) { if (noFaces) {
@@ -278,7 +323,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
return ( return (
<div <div
ref={gridRef} ref={gridWrapperRef}
style={{ style={{
overflow: 'hidden', overflow: 'hidden',
paddingBottom: '13px', paddingBottom: '13px',
@@ -286,6 +331,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
> >
{horizontal ? ( {horizontal ? (
<HorizontalGrid <HorizontalGrid
ref={gridRef}
isLoading={isLoading} isLoading={isLoading}
mangas={mangas} mangas={mangas}
inLibraryIndicator={inLibraryIndicator} inLibraryIndicator={inLibraryIndicator}
@@ -298,6 +344,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
/> />
) : ( ) : (
<VerticalGrid <VerticalGrid
ref={gridRef}
isLoading={isLoading} isLoading={isLoading}
mangas={mangas} mangas={mangas}
inLibraryIndicator={inLibraryIndicator} inLibraryIndicator={inLibraryIndicator}

View File

@@ -13,7 +13,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
import { useQueryParam, StringParam } from 'use-query-params'; import { useQueryParam, StringParam } from 'use-query-params';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import { Box, Button, styled, useTheme, useMediaQuery, Tooltip } from '@mui/material'; import { Box, Button, styled, Tooltip } from '@mui/material';
import FavoriteIcon from '@mui/icons-material/Favorite'; import FavoriteIcon from '@mui/icons-material/Favorite';
import NewReleasesIcon from '@mui/icons-material/NewReleases'; import NewReleasesIcon from '@mui/icons-material/NewReleases';
import FilterListIcon from '@mui/icons-material/FilterList'; import FilterListIcon from '@mui/icons-material/FilterList';
@@ -194,9 +194,6 @@ export function SourceMangas() {
const { t } = useTranslation(); const { t } = useTranslation();
const { setTitle, setAction } = useContext(NavBarContext); const { setTitle, setAction } = useContext(NavBarContext);
const theme = useTheme();
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));
const { sourceId } = useParams<{ sourceId: string }>(); const { sourceId } = useParams<{ sourceId: string }>();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -232,7 +229,7 @@ export function SourceMangas() {
contentType, contentType,
searchTerm, searchTerm,
filtersToApply, filtersToApply,
isLargeScreen ? 2 : 1, 1,
); );
const mangas = data?.fetchSourceManga.mangas ?? []; const mangas = data?.fetchSourceManga.mangas ?? [];
const hasNextPage = data?.fetchSourceManga.hasNextPage ?? false; const hasNextPage = data?.fetchSourceManga.hasNextPage ?? false;