Skip to content

Commit

Permalink
Feature/automatically load more pages until scrollbar is visible (#667)
Browse files Browse the repository at this point in the history
* Rename "grifRed" to "gridWrapperRef"

* Automatically fetch more pages until scrollbar is visible
  • Loading branch information
schroda authored Mar 23, 2024
1 parent 83635fa commit 66d6c95
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 124 deletions.
285 changes: 166 additions & 119 deletions src/components/MangaGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* 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 { Box, Typography } from '@mui/material';
import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso';
Expand Down Expand Up @@ -73,126 +73,139 @@ type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
};

const HorizontalGrid = ({
isLoading,
mangas,
inLibraryIndicator,
GridItemContainer,
gridLayout,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
}: DefaultGridProps) => (
<Grid
container
spacing={1}
style={{
margin: 0,
width: '100%',
padding: '5px',
overflowX: 'auto',
display: '-webkit-inline-box',
flexWrap: 'nowrap',
}}
>
{isLoading ? (
<LoadingPlaceholder />
) : (
mangas.map((manga) => (
<GridItemContainer key={manga.id}>
{createMangaCard(
manga,
gridLayout,
inLibraryIndicator,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)}
</GridItemContainer>
))
)}
</Grid>
const HorizontalGrid = forwardRef(
(
{
isLoading,
mangas,
inLibraryIndicator,
GridItemContainer,
gridLayout,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
}: DefaultGridProps,
ref: ForwardedRef<HTMLDivElement | null>,
) => (
<Grid
ref={ref}
container
spacing={1}
style={{
margin: 0,
width: '100%',
padding: '5px',
overflowX: 'auto',
display: '-webkit-inline-box',
flexWrap: 'nowrap',
}}
>
{isLoading ? (
<LoadingPlaceholder />
) : (
mangas.map((manga) => (
<GridItemContainer key={manga.id}>
{createMangaCard(
manga,
gridLayout,
inLibraryIndicator,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)}
</GridItemContainer>
))
)}
</Grid>
),
);

const VerticalGrid = ({
isLoading,
mangas,
inLibraryIndicator,
GridItemContainer,
gridLayout,
hasNextPage,
loadMore,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
}: DefaultGridProps & {
hasNextPage: boolean;
loadMore: () => void;
}) => {
const location = useLocation<{ snapshot?: GridStateSnapshot }>();
const navigate = useNavigate();
const { snapshot } = location.state ?? {};

const persistGridStateTimeout = useRef<NodeJS.Timeout | undefined>();
const persistGridState = (gridState: GridStateSnapshot) => {
const currentUrl = window.location.href;

clearTimeout(persistGridStateTimeout.current);
persistGridStateTimeout.current = setTimeout(() => {
const didLocationChange = currentUrl !== window.location.href;
if (didLocationChange) {
return;
}
const VerticalGrid = forwardRef(
(
{
isLoading,
mangas,
inLibraryIndicator,
GridItemContainer,
gridLayout,
hasNextPage,
loadMore,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
}: DefaultGridProps & {
hasNextPage: boolean;
loadMore: () => void;
},
ref: ForwardedRef<HTMLDivElement | null>,
) => {
const location = useLocation<{ snapshot?: GridStateSnapshot }>();
const navigate = useNavigate();
const { snapshot } = location.state ?? {};

navigate(
{ pathname: '', search: location.search },
{ replace: true, state: { ...location.state, snapshot: gridState } },
);
}, 250);
};
useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]);
const persistGridStateTimeout = useRef<NodeJS.Timeout | undefined>();
const persistGridState = (gridState: GridStateSnapshot) => {
const currentUrl = window.location.href;

return (
<>
<VirtuosoGrid
useWindowScroll
overscan={window.innerHeight * 0.25}
totalCount={mangas.length}
components={{
List: GridContainer,
Item: GridItemContainer,
}}
restoreStateFrom={snapshot}
stateChanged={persistGridState}
endReached={() => loadMore()}
itemContent={(index) =>
createMangaCard(
mangas[index],
gridLayout,
inLibraryIndicator,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)
clearTimeout(persistGridStateTimeout.current);
persistGridStateTimeout.current = setTimeout(() => {
const didLocationChange = currentUrl !== window.location.href;
if (didLocationChange) {
return;
}
/>
{/* render div to prevent UI jumping around when showing/hiding loading placeholder */
/* eslint-disable-next-line no-nested-ternary */}
{isSelectModeActive && gridLayout === GridLayout.List ? (
<Box sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }} />
) : // eslint-disable-next-line no-nested-ternary
isLoading ? (
<LoadingPlaceholder />
) : hasNextPage ? (
<div style={{ height: '75px' }} />
) : null}
</>
);
};

navigate(
{ pathname: '', search: location.search },
{ replace: true, state: { ...location.state, snapshot: gridState } },
);
}, 250);
};
useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]);

return (
<>
<Box ref={ref}>
<VirtuosoGrid
useWindowScroll
overscan={window.innerHeight * 0.25}
totalCount={mangas.length}
components={{
List: GridContainer,
Item: GridItemContainer,
}}
restoreStateFrom={snapshot}
stateChanged={persistGridState}
endReached={() => loadMore()}
itemContent={(index) =>
createMangaCard(
mangas[index],
gridLayout,
inLibraryIndicator,
isSelectModeActive,
selectedMangaIds,
handleSelection,
mode,
)
}
/>
</Box>
{/* render div to prevent UI jumping around when showing/hiding loading placeholder */
/* eslint-disable-next-line no-nested-ternary */}
{isSelectModeActive && gridLayout === GridLayout.List ? (
<Box sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }} />
) : // eslint-disable-next-line no-nested-ternary
isLoading ? (
<LoadingPlaceholder />
) : hasNextPage ? (
<div style={{ height: '75px' }} />
) : null}
</>
);
},
);

export interface IMangaGridProps extends Omit<DefaultGridProps, 'GridItemContainer'> {
message?: string;
Expand Down Expand Up @@ -221,17 +234,19 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
mode,
} = props;

const gridRef = useRef<HTMLDivElement>(null);

const [dimensions, setDimensions] = useState(document.documentElement.offsetWidth);
const [gridItemWidth] = useLocalStorage<number>('ItemWidth', 300);
const gridRef = useRef<HTMLDivElement>(null);
const gridWrapperRef = useRef<HTMLDivElement>(null);
const GridItemContainer = useMemo(
() => GridItemContainerWithDimension(dimensions, gridItemWidth, gridLayout),
[dimensions, gridItemWidth, gridLayout],
);

const updateGridWidth = () => {
const getDimensions = () => {
const gridWidth = gridRef.current?.offsetWidth;
const gridWidth = gridWrapperRef.current?.offsetWidth;

if (!gridWidth) {
return document.documentElement.offsetWidth;
Expand All @@ -258,6 +273,36 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
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;
if (hasNoItems) {
if (noFaces) {
Expand All @@ -278,14 +323,15 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {

return (
<div
ref={gridRef}
ref={gridWrapperRef}
style={{
overflow: 'hidden',
paddingBottom: '13px',
}}
>
{horizontal ? (
<HorizontalGrid
ref={gridRef}
isLoading={isLoading}
mangas={mangas}
inLibraryIndicator={inLibraryIndicator}
Expand All @@ -298,6 +344,7 @@ export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
/>
) : (
<VerticalGrid
ref={gridRef}
isLoading={isLoading}
mangas={mangas}
inLibraryIndicator={inLibraryIndicator}
Expand Down
7 changes: 2 additions & 5 deletions src/screens/SourceMangas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
import { useQueryParam, StringParam } from 'use-query-params';
import { useTranslation } from 'react-i18next';
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 NewReleasesIcon from '@mui/icons-material/NewReleases';
import FilterListIcon from '@mui/icons-material/FilterList';
Expand Down Expand Up @@ -194,9 +194,6 @@ export function SourceMangas() {
const { t } = useTranslation();
const { setTitle, setAction } = useContext(NavBarContext);

const theme = useTheme();
const isLargeScreen = useMediaQuery(theme.breakpoints.up('sm'));

const { sourceId } = useParams<{ sourceId: string }>();

const navigate = useNavigate();
Expand Down Expand Up @@ -232,7 +229,7 @@ export function SourceMangas() {
contentType,
searchTerm,
filtersToApply,
isLargeScreen ? 2 : 1,
1,
);
const mangas = data?.fetchSourceManga.mangas ?? [];
const hasNextPage = data?.fetchSourceManga.hasNextPage ?? false;
Expand Down

0 comments on commit 66d6c95

Please sign in to comment.