/* * Copyright (C) Contributors to the Suwayomi project * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import React, { 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'; import { useNavigate, useLocation } from 'react-router-dom'; import { EmptyView } from '@/components/util/EmptyView'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder'; import { MangaCard } from '@/components/MangaCard'; import { GridLayout } from '@/components/context/LibraryOptionsContext'; import { useLocalStorage } from '@/util/useLocalStorage'; import { TPartialManga } from '@/typings.ts'; const GridContainer = React.forwardRef(({ children, ...props }, ref) => ( {children} )); 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) => ( {children} ); }; const createMangaCard = (manga: TPartialManga, gridLayout?: GridLayout, inLibraryIndicator?: boolean) => ( ); type DefaultGridProps = { isLoading: boolean; mangas: TPartialManga[]; inLibraryIndicator?: boolean; GridItemContainer: (props: GridTypeMap['props'] & Partial) => JSX.Element; gridLayout?: GridLayout; }; const HorizontalGrid = ({ isLoading, mangas, inLibraryIndicator, GridItemContainer, gridLayout }: DefaultGridProps) => ( {isLoading ? ( ) : ( mangas.map((manga) => ( {createMangaCard(manga, gridLayout, inLibraryIndicator)} )) )} ); const VerticalGrid = ({ isLoading, mangas, inLibraryIndicator, GridItemContainer, gridLayout, hasNextPage, loadMore, }: DefaultGridProps & { hasNextPage: boolean; loadMore: () => void; }) => { const location = useLocation<{ snapshot?: GridStateSnapshot }>(); const navigate = useNavigate(); const { snapshot } = location.state ?? {}; const persistGridStateTimeout = useRef(); const persistGridState = (gridState: GridStateSnapshot) => { clearTimeout(persistGridStateTimeout.current); persistGridStateTimeout.current = setTimeout(() => { navigate( { pathname: '', search: location.search }, { replace: true, state: { ...location.state, snapshot: gridState } }, ); }, 250); }; useEffect(() => clearTimeout(persistGridStateTimeout.current), []); return ( <> 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 ? : hasNextPage ?
: null} ); }; export interface IMangaGridProps { mangas: TPartialManga[]; isLoading: boolean; message?: string; messageExtra?: JSX.Element; hasNextPage: boolean; loadMore: () => void; gridLayout?: GridLayout; horizontal?: boolean | undefined; noFaces?: boolean | undefined; inLibraryIndicator?: boolean; } export const MangaGrid: React.FC = (props) => { const { mangas, isLoading, message, messageExtra, hasNextPage, loadMore, gridLayout, horizontal, noFaces, inLibraryIndicator, } = props; const [dimensions, setDimensions] = useState(document.documentElement.offsetWidth); const [gridItemWidth] = useLocalStorage('ItemWidth', 300); const gridRef = useRef(null); const GridItemContainer = useMemo( () => GridItemContainerWithDimension(dimensions, gridItemWidth, gridLayout), [dimensions, gridItemWidth, gridLayout], ); const updateGridWidth = () => { setDimensions(gridRef.current?.offsetWidth ?? document.documentElement.offsetWidth); }; useLayoutEffect(updateGridWidth, []); useEffect(() => { let movementTimer: NodeJS.Timeout; const onResize = () => { clearInterval(movementTimer); movementTimer = setTimeout(updateGridWidth, 100); }; window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); const hasNoItems = !isLoading && mangas.length === 0; if (hasNoItems) { if (noFaces) { return ( {message} {messageExtra} ); } return ; } return (
{horizontal ? ( ) : ( )}
); }; MangaGrid.defaultProps = { message: '', messageExtra: undefined, };