2021-03-26 04:17:02 +04:30
|
|
|
/*
|
|
|
|
|
* Copyright (C) Contributors to the Suwayomi project
|
|
|
|
|
*
|
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
2021-01-26 23:32:12 +03:30
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
2023-05-18 13:17:41 +02:00
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
*/
|
2021-01-26 23:32:12 +03:30
|
|
|
|
2023-06-17 16:50:17 +02:00
|
|
|
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
|
import Grid, { GridTypeMap } from '@mui/material/Grid';
|
|
|
|
|
import { Box, Typography } from '@mui/material';
|
2023-12-10 15:58:28 +01:00
|
|
|
import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso';
|
2023-06-17 16:50:17 +02:00
|
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
2023-10-28 00:32:02 +02:00
|
|
|
import { EmptyView } from '@/components/util/EmptyView';
|
|
|
|
|
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder';
|
|
|
|
|
import { MangaCard } from '@/components/MangaCard';
|
2023-06-05 01:42:39 +02:00
|
|
|
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
2023-10-28 00:32:02 +02:00
|
|
|
import { useLocalStorage } from '@/util/useLocalStorage';
|
2023-10-15 16:03:08 +02:00
|
|
|
import { TPartialManga } from '@/typings.ts';
|
2023-06-17 16:50:17 +02:00
|
|
|
|
|
|
|
|
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => (
|
|
|
|
|
<Grid {...props} ref={ref} container sx={{ paddingLeft: '5px', paddingRight: '13px' }}>
|
|
|
|
|
{children}
|
|
|
|
|
</Grid>
|
|
|
|
|
));
|
|
|
|
|
|
2023-06-17 20:53:08 +02:00
|
|
|
const GridItemContainerWithDimension = (
|
|
|
|
|
dimensions: number,
|
|
|
|
|
itemWidth: number,
|
|
|
|
|
gridLayout?: GridLayout,
|
|
|
|
|
maxColumns: number = 12,
|
|
|
|
|
) => {
|
|
|
|
|
const itemsPerRow = Math.ceil(dimensions / itemWidth);
|
|
|
|
|
const columnsPerItem = gridLayout === GridLayout.List ? maxColumns : maxColumns / itemsPerRow;
|
|
|
|
|
|
|
|
|
|
return ({ children, ...itemProps }: GridTypeMap['props'] & Partial<GridItemProps>) => (
|
2023-06-17 20:54:55 +02:00
|
|
|
<Grid {...itemProps} item xs={columnsPerItem} sx={{ width: '100%', paddingTop: '8px', paddingLeft: '8px' }}>
|
2023-06-17 20:53:08 +02:00
|
|
|
{children}
|
|
|
|
|
</Grid>
|
|
|
|
|
);
|
|
|
|
|
};
|
2023-06-17 16:50:17 +02:00
|
|
|
|
2023-10-15 16:03:08 +02:00
|
|
|
const createMangaCard = (manga: TPartialManga, gridLayout?: GridLayout, inLibraryIndicator?: boolean) => (
|
2023-06-17 16:50:17 +02:00
|
|
|
<MangaCard key={manga.id} manga={manga} gridLayout={gridLayout} inLibraryIndicator={inLibraryIndicator} />
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
type DefaultGridProps = {
|
|
|
|
|
isLoading: boolean;
|
2023-10-15 16:03:08 +02:00
|
|
|
mangas: TPartialManga[];
|
2023-06-17 16:50:17 +02:00
|
|
|
inLibraryIndicator?: boolean;
|
|
|
|
|
GridItemContainer: (props: GridTypeMap['props'] & Partial<GridItemProps>) => JSX.Element;
|
|
|
|
|
gridLayout?: GridLayout;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const HorizontalGrid = ({ isLoading, mangas, inLibraryIndicator, GridItemContainer, gridLayout }: DefaultGridProps) => (
|
|
|
|
|
<Grid
|
|
|
|
|
container
|
|
|
|
|
spacing={1}
|
|
|
|
|
style={{
|
|
|
|
|
margin: 0,
|
|
|
|
|
width: '100%',
|
|
|
|
|
padding: '5px',
|
2023-06-17 20:53:44 +02:00
|
|
|
overflowX: 'auto',
|
2023-06-17 16:50:17 +02:00
|
|
|
display: '-webkit-inline-box',
|
|
|
|
|
flexWrap: 'nowrap',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<LoadingPlaceholder />
|
|
|
|
|
) : (
|
|
|
|
|
mangas.map((manga) => (
|
|
|
|
|
<GridItemContainer key={manga.id}>
|
|
|
|
|
{createMangaCard(manga, gridLayout, inLibraryIndicator)}
|
|
|
|
|
</GridItemContainer>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</Grid>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const VerticalGrid = ({
|
|
|
|
|
isLoading,
|
|
|
|
|
mangas,
|
|
|
|
|
inLibraryIndicator,
|
|
|
|
|
GridItemContainer,
|
|
|
|
|
gridLayout,
|
|
|
|
|
hasNextPage,
|
|
|
|
|
loadMore,
|
|
|
|
|
}: DefaultGridProps & {
|
|
|
|
|
hasNextPage: boolean;
|
|
|
|
|
loadMore: () => void;
|
|
|
|
|
}) => {
|
2023-12-10 15:58:28 +01:00
|
|
|
const location = useLocation<{ snapshot?: GridStateSnapshot }>();
|
2023-06-17 16:50:17 +02:00
|
|
|
const navigate = useNavigate();
|
2023-12-10 15:58:28 +01:00
|
|
|
const { snapshot } = location.state ?? {};
|
2023-06-17 16:50:17 +02:00
|
|
|
|
2023-12-10 15:58:28 +01:00
|
|
|
const persistGridStateTimeout = useRef<NodeJS.Timeout | undefined>();
|
|
|
|
|
const persistGridState = (gridState: GridStateSnapshot) => {
|
|
|
|
|
clearTimeout(persistGridStateTimeout.current);
|
|
|
|
|
persistGridStateTimeout.current = setTimeout(() => {
|
2023-06-17 16:50:17 +02:00
|
|
|
navigate(
|
|
|
|
|
{ pathname: '', search: location.search },
|
2023-12-10 15:58:28 +01:00
|
|
|
{ replace: true, state: { ...location.state, snapshot: gridState } },
|
2023-06-17 16:50:17 +02:00
|
|
|
);
|
2023-12-10 15:58:28 +01:00
|
|
|
}, 250);
|
|
|
|
|
};
|
|
|
|
|
useEffect(() => clearTimeout(persistGridStateTimeout.current), []);
|
2023-06-17 16:50:17 +02:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<VirtuosoGrid
|
|
|
|
|
useWindowScroll
|
|
|
|
|
overscan={window.innerHeight * 0.25}
|
|
|
|
|
totalCount={mangas.length}
|
|
|
|
|
components={{
|
|
|
|
|
List: GridContainer,
|
|
|
|
|
Item: GridItemContainer,
|
|
|
|
|
}}
|
2023-12-10 15:58:28 +01:00
|
|
|
restoreStateFrom={snapshot}
|
|
|
|
|
stateChanged={persistGridState}
|
2023-06-17 16:50:17 +02:00
|
|
|
endReached={() => loadMore()}
|
|
|
|
|
itemContent={(index) => createMangaCard(mangas[index], gridLayout, inLibraryIndicator)}
|
|
|
|
|
/>
|
|
|
|
|
{/* render div to prevent UI jumping around when showing/hiding loading placeholder */
|
|
|
|
|
/* eslint-disable-next-line no-nested-ternary */}
|
|
|
|
|
{isLoading ? <LoadingPlaceholder /> : hasNextPage ? <div style={{ height: '75px' }} /> : null}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
2021-01-20 15:26:52 +03:30
|
|
|
|
2023-02-06 10:06:33 +01:00
|
|
|
export interface IMangaGridProps {
|
2023-10-15 16:03:08 +02:00
|
|
|
mangas: TPartialManga[];
|
2023-02-06 10:06:33 +01:00
|
|
|
isLoading: boolean;
|
|
|
|
|
message?: string;
|
|
|
|
|
messageExtra?: JSX.Element;
|
|
|
|
|
hasNextPage: boolean;
|
2023-06-17 16:50:17 +02:00
|
|
|
loadMore: () => void;
|
2023-02-06 10:06:33 +01:00
|
|
|
gridLayout?: GridLayout;
|
2023-02-12 20:43:41 +05:30
|
|
|
horizontal?: boolean | undefined;
|
2023-02-06 10:06:33 +01:00
|
|
|
noFaces?: boolean | undefined;
|
|
|
|
|
inLibraryIndicator?: boolean;
|
2021-01-20 15:26:52 +03:30
|
|
|
}
|
|
|
|
|
|
2023-10-28 00:32:02 +02:00
|
|
|
export const MangaGrid: React.FC<IMangaGridProps> = (props) => {
|
2021-01-22 21:11:00 +03:30
|
|
|
const {
|
2023-02-06 10:06:33 +01:00
|
|
|
mangas,
|
|
|
|
|
isLoading,
|
|
|
|
|
message,
|
|
|
|
|
messageExtra,
|
|
|
|
|
hasNextPage,
|
2023-06-17 16:50:17 +02:00
|
|
|
loadMore,
|
2023-02-06 10:06:33 +01:00
|
|
|
gridLayout,
|
2023-02-12 20:43:41 +05:30
|
|
|
horizontal,
|
2023-02-06 10:06:33 +01:00
|
|
|
noFaces,
|
2022-11-24 21:04:30 +01:00
|
|
|
inLibraryIndicator,
|
2021-01-22 21:11:00 +03:30
|
|
|
} = props;
|
2022-04-02 01:34:10 +01:00
|
|
|
|
2023-06-17 22:31:59 +02:00
|
|
|
const [dimensions, setDimensions] = useState(document.documentElement.offsetWidth);
|
2023-06-17 16:50:17 +02:00
|
|
|
const [gridItemWidth] = useLocalStorage<number>('ItemWidth', 300);
|
2022-04-02 01:34:10 +01:00
|
|
|
const gridRef = useRef<HTMLDivElement>(null);
|
2023-06-17 16:50:17 +02:00
|
|
|
const GridItemContainer = useMemo(
|
2023-06-17 22:31:59 +02:00
|
|
|
() => GridItemContainerWithDimension(dimensions, gridItemWidth, gridLayout),
|
2023-06-17 16:50:17 +02:00
|
|
|
[dimensions, gridItemWidth, gridLayout],
|
|
|
|
|
);
|
2022-04-02 01:34:10 +01:00
|
|
|
|
2023-06-17 16:50:17 +02:00
|
|
|
const updateGridWidth = () => {
|
2023-06-17 22:31:59 +02:00
|
|
|
setDimensions(gridRef.current?.offsetWidth ?? document.documentElement.offsetWidth);
|
2022-04-02 01:34:10 +01:00
|
|
|
};
|
|
|
|
|
|
2023-06-17 22:31:59 +02:00
|
|
|
useLayoutEffect(updateGridWidth, []);
|
2022-04-02 01:34:10 +01:00
|
|
|
|
2023-06-17 16:50:17 +02:00
|
|
|
useEffect(() => {
|
|
|
|
|
let movementTimer: NodeJS.Timeout;
|
|
|
|
|
|
|
|
|
|
const onResize = () => {
|
|
|
|
|
clearInterval(movementTimer);
|
|
|
|
|
movementTimer = setTimeout(updateGridWidth, 100);
|
|
|
|
|
};
|
2022-04-02 01:34:10 +01:00
|
|
|
|
2023-06-17 16:50:17 +02:00
|
|
|
window.addEventListener('resize', onResize);
|
2022-04-02 01:34:10 +01:00
|
|
|
|
2023-06-17 16:50:17 +02:00
|
|
|
return () => window.removeEventListener('resize', onResize);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const hasNoItems = !isLoading && mangas.length === 0;
|
|
|
|
|
if (hasNoItems) {
|
|
|
|
|
if (noFaces) {
|
|
|
|
|
return (
|
2022-03-12 04:46:55 +00:00
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
margin: 'auto',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2023-02-06 10:06:33 +01:00
|
|
|
<Typography variant="h5">{message}</Typography>
|
2022-03-12 04:46:55 +00:00
|
|
|
{messageExtra}
|
|
|
|
|
</Box>
|
2021-10-24 22:56:10 +03:30
|
|
|
);
|
|
|
|
|
}
|
2023-06-17 16:50:17 +02:00
|
|
|
|
|
|
|
|
return <EmptyView message={message!} messageExtra={messageExtra} />;
|
2021-01-20 15:26:52 +03:30
|
|
|
}
|
|
|
|
|
|
2021-01-22 21:11:00 +03:30
|
|
|
return (
|
2023-06-17 16:50:17 +02:00
|
|
|
<div
|
|
|
|
|
ref={gridRef}
|
|
|
|
|
style={{
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
paddingBottom: '13px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{horizontal ? (
|
|
|
|
|
<HorizontalGrid
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
mangas={mangas}
|
|
|
|
|
inLibraryIndicator={inLibraryIndicator}
|
|
|
|
|
GridItemContainer={GridItemContainer}
|
|
|
|
|
gridLayout={gridLayout}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<VerticalGrid
|
|
|
|
|
isLoading={isLoading}
|
|
|
|
|
mangas={mangas}
|
2023-08-16 19:07:54 +02:00
|
|
|
inLibraryIndicator={inLibraryIndicator}
|
2023-06-17 16:50:17 +02:00
|
|
|
GridItemContainer={GridItemContainer}
|
|
|
|
|
hasNextPage={hasNextPage}
|
|
|
|
|
loadMore={loadMore}
|
|
|
|
|
gridLayout={gridLayout}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2022-04-02 01:34:10 +01:00
|
|
|
</div>
|
2021-01-22 21:11:00 +03:30
|
|
|
);
|
2022-11-24 21:04:30 +01:00
|
|
|
};
|
2021-01-22 21:11:00 +03:30
|
|
|
|
|
|
|
|
MangaGrid.defaultProps = {
|
2021-10-24 22:56:10 +03:30
|
|
|
message: '',
|
2021-09-19 17:30:34 +04:30
|
|
|
messageExtra: undefined,
|
2021-01-22 21:11:00 +03:30
|
|
|
};
|