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:
@@ -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,126 +73,139 @@ type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
|
|||||||
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
|
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
|
||||||
};
|
};
|
||||||
|
|
||||||
const HorizontalGrid = ({
|
const HorizontalGrid = forwardRef(
|
||||||
isLoading,
|
(
|
||||||
mangas,
|
{
|
||||||
inLibraryIndicator,
|
isLoading,
|
||||||
GridItemContainer,
|
mangas,
|
||||||
gridLayout,
|
inLibraryIndicator,
|
||||||
isSelectModeActive,
|
GridItemContainer,
|
||||||
selectedMangaIds,
|
gridLayout,
|
||||||
handleSelection,
|
isSelectModeActive,
|
||||||
mode,
|
selectedMangaIds,
|
||||||
}: DefaultGridProps) => (
|
handleSelection,
|
||||||
<Grid
|
mode,
|
||||||
container
|
}: DefaultGridProps,
|
||||||
spacing={1}
|
ref: ForwardedRef<HTMLDivElement | null>,
|
||||||
style={{
|
) => (
|
||||||
margin: 0,
|
<Grid
|
||||||
width: '100%',
|
ref={ref}
|
||||||
padding: '5px',
|
container
|
||||||
overflowX: 'auto',
|
spacing={1}
|
||||||
display: '-webkit-inline-box',
|
style={{
|
||||||
flexWrap: 'nowrap',
|
margin: 0,
|
||||||
}}
|
width: '100%',
|
||||||
>
|
padding: '5px',
|
||||||
{isLoading ? (
|
overflowX: 'auto',
|
||||||
<LoadingPlaceholder />
|
display: '-webkit-inline-box',
|
||||||
) : (
|
flexWrap: 'nowrap',
|
||||||
mangas.map((manga) => (
|
}}
|
||||||
<GridItemContainer key={manga.id}>
|
>
|
||||||
{createMangaCard(
|
{isLoading ? (
|
||||||
manga,
|
<LoadingPlaceholder />
|
||||||
gridLayout,
|
) : (
|
||||||
inLibraryIndicator,
|
mangas.map((manga) => (
|
||||||
isSelectModeActive,
|
<GridItemContainer key={manga.id}>
|
||||||
selectedMangaIds,
|
{createMangaCard(
|
||||||
handleSelection,
|
manga,
|
||||||
mode,
|
gridLayout,
|
||||||
)}
|
inLibraryIndicator,
|
||||||
</GridItemContainer>
|
isSelectModeActive,
|
||||||
))
|
selectedMangaIds,
|
||||||
)}
|
handleSelection,
|
||||||
</Grid>
|
mode,
|
||||||
|
)}
|
||||||
|
</GridItemContainer>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const VerticalGrid = ({
|
const VerticalGrid = forwardRef(
|
||||||
isLoading,
|
(
|
||||||
mangas,
|
{
|
||||||
inLibraryIndicator,
|
isLoading,
|
||||||
GridItemContainer,
|
mangas,
|
||||||
gridLayout,
|
inLibraryIndicator,
|
||||||
hasNextPage,
|
GridItemContainer,
|
||||||
loadMore,
|
gridLayout,
|
||||||
isSelectModeActive,
|
hasNextPage,
|
||||||
selectedMangaIds,
|
loadMore,
|
||||||
handleSelection,
|
isSelectModeActive,
|
||||||
mode,
|
selectedMangaIds,
|
||||||
}: DefaultGridProps & {
|
handleSelection,
|
||||||
hasNextPage: boolean;
|
mode,
|
||||||
loadMore: () => void;
|
}: DefaultGridProps & {
|
||||||
}) => {
|
hasNextPage: boolean;
|
||||||
const location = useLocation<{ snapshot?: GridStateSnapshot }>();
|
loadMore: () => void;
|
||||||
const navigate = useNavigate();
|
},
|
||||||
const { snapshot } = location.state ?? {};
|
ref: ForwardedRef<HTMLDivElement | null>,
|
||||||
|
) => {
|
||||||
|
const location = useLocation<{ snapshot?: GridStateSnapshot }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { snapshot } = location.state ?? {};
|
||||||
|
|
||||||
const persistGridStateTimeout = useRef<NodeJS.Timeout | undefined>();
|
const persistGridStateTimeout = useRef<NodeJS.Timeout | undefined>();
|
||||||
const persistGridState = (gridState: GridStateSnapshot) => {
|
const persistGridState = (gridState: GridStateSnapshot) => {
|
||||||
const currentUrl = window.location.href;
|
const currentUrl = window.location.href;
|
||||||
|
|
||||||
clearTimeout(persistGridStateTimeout.current);
|
clearTimeout(persistGridStateTimeout.current);
|
||||||
persistGridStateTimeout.current = setTimeout(() => {
|
persistGridStateTimeout.current = setTimeout(() => {
|
||||||
const didLocationChange = currentUrl !== window.location.href;
|
const didLocationChange = currentUrl !== window.location.href;
|
||||||
if (didLocationChange) {
|
if (didLocationChange) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
{ pathname: '', search: location.search },
|
|
||||||
{ replace: true, state: { ...location.state, snapshot: gridState } },
|
|
||||||
);
|
|
||||||
}, 250);
|
|
||||||
};
|
|
||||||
useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]);
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
{/* render div to prevent UI jumping around when showing/hiding loading placeholder */
|
navigate(
|
||||||
/* eslint-disable-next-line no-nested-ternary */}
|
{ pathname: '', search: location.search },
|
||||||
{isSelectModeActive && gridLayout === GridLayout.List ? (
|
{ replace: true, state: { ...location.state, snapshot: gridState } },
|
||||||
<Box sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }} />
|
);
|
||||||
) : // eslint-disable-next-line no-nested-ternary
|
}, 250);
|
||||||
isLoading ? (
|
};
|
||||||
<LoadingPlaceholder />
|
useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]);
|
||||||
) : hasNextPage ? (
|
|
||||||
<div style={{ height: '75px' }} />
|
return (
|
||||||
) : null}
|
<>
|
||||||
</>
|
<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'> {
|
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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user