/* * 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, VirtuosoGrid, VirtuosoGridHandle } from 'react-virtuoso'; import { useNavigate, useLocation } from 'react-router-dom'; import { IMangaCard } from '@/typings'; 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'; 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: IMangaCard, gridLayout?: GridLayout, inLibraryIndicator?: boolean) => ( ); type DefaultGridProps = { isLoading: boolean; mangas: IMangaCard[]; 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 [restoredScrollPosition, setRestoredScrollPosition] = useState(mangas.length === 0); const location = useLocation<{ lastScrollPosition?: number }>(); const navigate = useNavigate(); const virtuoso = useRef(null); const { lastScrollPosition = 0 } = location.state ?? {}; useEffect(() => { const updateLastScrollPosition = () => { if (!restoredScrollPosition) { return; } navigate( { pathname: '', search: location.search }, { replace: true, state: { ...location.state, lastScrollPosition: window.scrollY } }, ); }; window.addEventListener('scroll', updateLastScrollPosition, true); window.addEventListener('resize', updateLastScrollPosition, true); return () => { window.removeEventListener('scroll', updateLastScrollPosition, true); window.removeEventListener('resize', updateLastScrollPosition, true); }; }, [restoredScrollPosition, location.state, location.search]); useEffect(() => { const haveItemsRendered = document.documentElement.offsetHeight >= lastScrollPosition; const restoreScrollPosition = !restoredScrollPosition && haveItemsRendered && virtuoso.current; if (!restoreScrollPosition) { return; } virtuoso.current.scrollTo({ top: lastScrollPosition }); setRestoredScrollPosition(true); }, [document.documentElement.offsetHeight, virtuoso.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: IMangaCard[]; isLoading: boolean; message?: string; messageExtra?: JSX.Element; hasNextPage: boolean; loadMore: () => void; gridLayout?: GridLayout; horizontal?: boolean | undefined; noFaces?: boolean | undefined; inLibraryIndicator?: boolean; } const MangaGrid: React.FC = (props) => { const { mangas, isLoading, message, messageExtra, hasNextPage, loadMore, gridLayout, horizontal, noFaces, inLibraryIndicator, } = props; const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const [gridItemWidth] = useLocalStorage('ItemWidth', 300); const gridRef = useRef(null); const GridItemContainer = useMemo( () => GridItemContainerWithDimension(dimensions.width, gridItemWidth, gridLayout), [dimensions, gridItemWidth, gridLayout], ); const updateGridWidth = () => { setDimensions({ width: gridRef.current?.offsetWidth ?? 0, height: gridRef.current?.offsetHeight ?? 0, }); }; useLayoutEffect(() => updateGridWidth, [gridRef.current?.offsetWidth, gridRef.current?.offsetHeight]); 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, }; export default MangaGrid;