diff --git a/src/lib/virtuoso/Component/GroupedVirtuosoPersisted.tsx b/src/lib/virtuoso/Component/GroupedVirtuosoPersisted.tsx new file mode 100644 index 00000000..0778193d --- /dev/null +++ b/src/lib/virtuoso/Component/GroupedVirtuosoPersisted.tsx @@ -0,0 +1,38 @@ +/* + * 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 { ComponentProps, useRef } from 'react'; +import { GroupedVirtuoso, GroupedVirtuosoHandle, StateSnapshot } from 'react-virtuoso'; +import { useMergedRef } from '@mantine/hooks'; +import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; + +export const GroupedVirtuosoPersisted = ({ + ref: passedRef, + persistKey, + ...props +}: ComponentProps & { persistKey: string }) => { + const { state, persistState } = VirtuosoUtil.usePersistState(persistKey); + + const localRef = useRef(undefined); + const ref = useMergedRef(localRef, passedRef); + + return ( + { + if (!isScrolling) { + localRef.current?.getState(persistState); + } + + props.isScrolling?.(isScrolling); + }} + restoreStateFrom={state} + /> + ); +}; diff --git a/src/lib/virtuoso/Component/VirtuosoGridPersisted.tsx b/src/lib/virtuoso/Component/VirtuosoGridPersisted.tsx new file mode 100644 index 00000000..da8db902 --- /dev/null +++ b/src/lib/virtuoso/Component/VirtuosoGridPersisted.tsx @@ -0,0 +1,25 @@ +/* + * 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 { ComponentProps, useRef } from 'react'; +import { GridStateSnapshot, VirtuosoGrid, VirtuosoGridHandle } from 'react-virtuoso'; +import { useMergedRef } from '@mantine/hooks'; +import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; + +export const VirtuosoGridPersisted = ({ + ref: passedRef, + persistKey, + ...props +}: ComponentProps & { persistKey: string }) => { + const { state, persistState } = VirtuosoUtil.usePersistState(persistKey); + + const localRef = useRef(undefined); + const ref = useMergedRef(localRef, passedRef); + + return ; +}; diff --git a/src/lib/virtuoso/Component/VirtuosoPersisted.tsx b/src/lib/virtuoso/Component/VirtuosoPersisted.tsx new file mode 100644 index 00000000..9119bca1 --- /dev/null +++ b/src/lib/virtuoso/Component/VirtuosoPersisted.tsx @@ -0,0 +1,38 @@ +/* + * 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 { ComponentProps, useRef } from 'react'; +import { StateSnapshot, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; +import { useMergedRef } from '@mantine/hooks'; +import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; + +export const VirtuosoPersisted = ({ + ref: passedRef, + persistKey, + ...props +}: ComponentProps & { persistKey: string }) => { + const { state, persistState } = VirtuosoUtil.usePersistState(persistKey); + + const localRef = useRef(undefined); + const ref = useMergedRef(localRef, passedRef); + + return ( + { + if (!isScrolling) { + localRef.current?.getState(persistState); + } + + props.isScrolling?.(isScrolling); + }} + restoreStateFrom={state} + /> + ); +}; diff --git a/src/lib/virtuoso/Virtuoso.util.tsx b/src/lib/virtuoso/Virtuoso.util.tsx index 9bfa0220..a96ea31b 100644 --- a/src/lib/virtuoso/Virtuoso.util.tsx +++ b/src/lib/virtuoso/Virtuoso.util.tsx @@ -6,7 +6,10 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useSessionStorage } from '@/modules/core/hooks/useStorage.tsx'; +import { AppStorage } from '@/lib/storage/AppStorage.ts'; export class VirtuosoUtil { static readonly GROUP = 0; @@ -112,4 +115,46 @@ export class VirtuosoUtil { [convertIndex, getGroupKey, getNormalKey], ); } + + static usePersistState(key: string): { + key: string; + state: Snapshot | undefined; + persistState: (state: Snapshot) => void; + deleteState: () => void; + } { + const location = useLocation<{ snapshot?: Snapshot }>(); + + const snapshotSessionKey = `virtuoso-snapshot-${key}-${location.key}`; + const [snapshot] = useSessionStorage(snapshotSessionKey, undefined); + + const persistGridStateTimeout = useRef(undefined); + const persistState = useCallback( + (state: Snapshot) => { + const currentUrl = window.location.href; + + clearTimeout(persistGridStateTimeout.current); + persistGridStateTimeout.current = setTimeout(() => { + const didLocationChange = currentUrl !== window.location.href; + if (didLocationChange) { + return; + } + + AppStorage.session.setItem(snapshotSessionKey, state, false); + }, 250); + }, + [snapshotSessionKey], + ); + const deleteState = useCallback(() => { + AppStorage.session.setItem(snapshotSessionKey, undefined, false); + }, [snapshotSessionKey]); + + useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]); + + return { + key: snapshotSessionKey, + state: snapshot, + persistState, + deleteState, + }; + } } diff --git a/src/modules/chapter/components/ChapterList.tsx b/src/modules/chapter/components/ChapterList.tsx index 0f57a011..dae6290c 100644 --- a/src/modules/chapter/components/ChapterList.tsx +++ b/src/modules/chapter/components/ChapterList.tsx @@ -11,7 +11,6 @@ import Stack from '@mui/material/Stack'; import { styled } from '@mui/material/styles'; import Typography from '@mui/material/Typography'; import { ComponentProps, useCallback, useMemo, useState } from 'react'; -import { Virtuoso } from 'react-virtuoso'; import { useTranslation } from 'react-i18next'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { ResumeFab } from '@/modules/manga/components/ResumeFAB.tsx'; @@ -45,6 +44,7 @@ import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { ChapterListCard } from '@/modules/chapter/components/cards/ChapterListCard.tsx'; +import { VirtuosoPersisted } from '@/lib/virtuoso/Component/VirtuosoPersisted.tsx'; type ChapterListHeaderProps = { scrollbarWidth: number; @@ -61,7 +61,7 @@ const ChapterListHeader = styled(Stack, { })); type StyledVirtuosoProps = { topOffset: number }; -const StyledVirtuoso = styled(Virtuoso, { +const StyledVirtuoso = styled(VirtuosoPersisted, { shouldForwardProp: shouldForwardProp(['topOffset']), })(({ theme, topOffset }) => ({ listStyle: 'none', @@ -234,6 +234,7 @@ export const ChapterList = ({ )} @@ -23,7 +24,7 @@ export const StyledGroupedVirtuoso = ({ heightToSubtract = 0, style, ...props -}: ComponentProps & { heightToSubtract?: number }) => { +}: ComponentProps & { heightToSubtract?: number }) => { const { appBarHeight, bottomBarHeight } = useNavBarContext(); const TopItemList = useMemo( @@ -32,7 +33,7 @@ export const StyledGroupedVirtuoso = ({ ); return ( - { const { t } = useTranslation(); @@ -159,7 +159,8 @@ export const DownloadQueue: React.FC = () => { onDragAbort={() => setDndActiveDownload(null)} > - { return ( (isLoading ? : null), }} diff --git a/src/modules/library/screens/LibraryDuplicates.tsx b/src/modules/library/screens/LibraryDuplicates.tsx index 353bdb59..5b535f6f 100644 --- a/src/modules/library/screens/LibraryDuplicates.tsx +++ b/src/modules/library/screens/LibraryDuplicates.tsx @@ -140,6 +140,7 @@ export const LibraryDuplicates = () => { if (gridLayout === GridLayout.List) { return ( ( diff --git a/src/modules/manga/components/MangaGrid.tsx b/src/modules/manga/components/MangaGrid.tsx index a91ca275..d748049c 100644 --- a/src/modules/manga/components/MangaGrid.tsx +++ b/src/modules/manga/components/MangaGrid.tsx @@ -10,7 +10,6 @@ import React, { ForwardedRef, forwardRef, useCallback, - useEffect, useLayoutEffect, useMemo, useRef, @@ -19,22 +18,20 @@ import React, { } from 'react'; import Grid, { GridTypeMap } from '@mui/material/Grid'; import Box, { BoxProps } from '@mui/material/Box'; -import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso'; -import { useLocation } from 'react-router-dom'; +import { GridItemProps } from 'react-virtuoso'; import { useTranslation } from 'react-i18next'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/feedback/EmptyViewAbsoluteCentered.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/feedback/LoadingPlaceholder.tsx'; import { MangaCard } from '@/modules/manga/components/cards/MangaCard.tsx'; -import { useSessionStorage } from '@/modules/core/hooks/useStorage.tsx'; import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx'; -import { AppStorage } from '@/lib/storage/AppStorage.ts'; import { MangaCardProps } from '@/modules/manga/Manga.types.ts'; import { MangaType } from '@/lib/graphql/generated/graphql.ts'; import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { GridLayout } from '@/modules/core/Core.types.ts'; import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; +import { VirtuosoGridPersisted } from '@/lib/virtuoso/Component/VirtuosoGridPersisted.tsx'; const GridContainer = React.forwardRef(({ children, ...props }, ref) => ( @@ -138,8 +135,7 @@ const HorizontalGrid = forwardRef( ), ); -export const getGridSnapshotKey = (location: ReturnType) => - `MangaGrid-snapshot-location-${location.key}`; +export const MANGA_GRID_SNAPSHOT_KEY = 'MangaGrid-snapshot-location'; const VerticalGrid = forwardRef( ( @@ -160,69 +156,45 @@ const VerticalGrid = forwardRef( loadMore: () => void; }, ref: ForwardedRef, - ) => { - const location = useLocation<{ snapshot?: GridStateSnapshot }>(); - - const snapshotSessionKey = getGridSnapshotKey(location); - const [snapshot] = useSessionStorage(snapshotSessionKey, undefined); - - const persistGridStateTimeout = useRef(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; - } - - AppStorage.session.setItem(snapshotSessionKey, gridState, false); - }, 250); - }; - useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]); - - return ( - <> - - loadMore()} - computeItemKey={(index) => mangas[index].id} - itemContent={(index) => - createMangaCard( - mangas[index], - gridLayout, - inLibraryIndicator, - isSelectModeActive, - selectedMangaIds, - handleSelection, - mode, - ) - } - /> - - {/* render div to prevent UI jumping around when showing/hiding loading placeholder */ - /* eslint-disable-next-line no-nested-ternary */} - {isSelectModeActive && gridLayout === GridLayout.List ? ( - - ) : // eslint-disable-next-line no-nested-ternary - isLoading ? ( - - ) : hasNextPage ? ( -
- ) : null} - - ); - }, + ) => ( + <> + + loadMore()} + computeItemKey={(index) => mangas[index].id} + itemContent={(index) => + createMangaCard( + mangas[index], + gridLayout, + inLibraryIndicator, + isSelectModeActive, + selectedMangaIds, + handleSelection, + mode, + ) + } + /> + + {/* render div to prevent UI jumping around when showing/hiding loading placeholder */ + /* eslint-disable-next-line no-nested-ternary */} + {isSelectModeActive && gridLayout === GridLayout.List ? ( + + ) : // eslint-disable-next-line no-nested-ternary + isLoading ? ( + + ) : hasNextPage ? ( +
+ ) : null} + + ), ); export interface IMangaGridProps diff --git a/src/modules/source/screens/SourceMangas.tsx b/src/modules/source/screens/SourceMangas.tsx index eb30fe07..417626e8 100644 --- a/src/modules/source/screens/SourceMangas.tsx +++ b/src/modules/source/screens/SourceMangas.tsx @@ -40,8 +40,7 @@ import { useMetadataServerSettings, } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { useLocalStorage, useSessionStorage } from '@/modules/core/hooks/useStorage.tsx'; -import { AppStorage } from '@/lib/storage/AppStorage.ts'; -import { getGridSnapshotKey } from '@/modules/manga/components/MangaGrid.tsx'; +import { MANGA_GRID_SNAPSHOT_KEY } from '@/modules/manga/components/MangaGrid.tsx'; import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/modules/source/services/SourceMetadata.ts'; import { makeToast } from '@/modules/core/utils/Toast.ts'; import { GET_SOURCE_BROWSE } from '@/lib/graphql/queries/SourceQuery.ts'; @@ -57,6 +56,7 @@ import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { Sources } from '@/modules/source/services/Sources.ts'; import { useAppTitleAndAction } from '@/modules/navigation-bar/hooks/useAppTitleAndAction.ts'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; +import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; const DEFAULT_SOURCE: SourceIdInfo = { id: '-1' }; @@ -246,10 +246,12 @@ export function SourceMangas() { query ? SourceContentType.SEARCH : currentContentType!, ); + const { key: persistedGridStateKey, deleteState: deletePersistedGridState } = + VirtuosoUtil.usePersistState(MANGA_GRID_SNAPSHOT_KEY); const scrollToTop = useCallback(() => { - AppStorage.session.setItem(getGridSnapshotKey(location), undefined, false); + deletePersistedGridState(); window.scrollTo(0, 0); - }, [locationKey]); + }, [persistedGridStateKey]); const currentQuery = useRef(query); const currentAbortRequest = useRef<(reason: any) => void>(() => {}); diff --git a/src/modules/updates/screens/Updates.tsx b/src/modules/updates/screens/Updates.tsx index b85b1320..dd0cc10f 100644 --- a/src/modules/updates/screens/Updates.tsx +++ b/src/modules/updates/screens/Updates.tsx @@ -114,6 +114,7 @@ export const Updates: React.FC = () => { })} (isLoading ? : null),