Restore virtuoso states on browser back navigation
- manga chapter list - library duplicates - updates - history - download queue
This commit is contained in:
38
src/lib/virtuoso/Component/GroupedVirtuosoPersisted.tsx
Normal file
38
src/lib/virtuoso/Component/GroupedVirtuosoPersisted.tsx
Normal file
@@ -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<typeof GroupedVirtuoso> & { persistKey: string }) => {
|
||||||
|
const { state, persistState } = VirtuosoUtil.usePersistState<StateSnapshot>(persistKey);
|
||||||
|
|
||||||
|
const localRef = useRef<GroupedVirtuosoHandle>(undefined);
|
||||||
|
const ref = useMergedRef(localRef, passedRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GroupedVirtuoso
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
isScrolling={(isScrolling) => {
|
||||||
|
if (!isScrolling) {
|
||||||
|
localRef.current?.getState(persistState);
|
||||||
|
}
|
||||||
|
|
||||||
|
props.isScrolling?.(isScrolling);
|
||||||
|
}}
|
||||||
|
restoreStateFrom={state}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
25
src/lib/virtuoso/Component/VirtuosoGridPersisted.tsx
Normal file
25
src/lib/virtuoso/Component/VirtuosoGridPersisted.tsx
Normal file
@@ -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<typeof VirtuosoGrid> & { persistKey: string }) => {
|
||||||
|
const { state, persistState } = VirtuosoUtil.usePersistState<GridStateSnapshot>(persistKey);
|
||||||
|
|
||||||
|
const localRef = useRef<VirtuosoGridHandle>(undefined);
|
||||||
|
const ref = useMergedRef(localRef, passedRef);
|
||||||
|
|
||||||
|
return <VirtuosoGrid {...props} ref={ref} stateChanged={persistState} restoreStateFrom={state} />;
|
||||||
|
};
|
||||||
38
src/lib/virtuoso/Component/VirtuosoPersisted.tsx
Normal file
38
src/lib/virtuoso/Component/VirtuosoPersisted.tsx
Normal file
@@ -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<typeof Virtuoso> & { persistKey: string }) => {
|
||||||
|
const { state, persistState } = VirtuosoUtil.usePersistState<StateSnapshot>(persistKey);
|
||||||
|
|
||||||
|
const localRef = useRef<VirtuosoHandle>(undefined);
|
||||||
|
const ref = useMergedRef(localRef, passedRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Virtuoso
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
isScrolling={(isScrolling) => {
|
||||||
|
if (!isScrolling) {
|
||||||
|
localRef.current?.getState(persistState);
|
||||||
|
}
|
||||||
|
|
||||||
|
props.isScrolling?.(isScrolling);
|
||||||
|
}}
|
||||||
|
restoreStateFrom={state}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,7 +6,10 @@
|
|||||||
* 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 { 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 {
|
export class VirtuosoUtil {
|
||||||
static readonly GROUP = 0;
|
static readonly GROUP = 0;
|
||||||
@@ -112,4 +115,46 @@ export class VirtuosoUtil {
|
|||||||
[convertIndex, getGroupKey, getNormalKey],
|
[convertIndex, getGroupKey, getNormalKey],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static usePersistState<Snapshot>(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<Snapshot | undefined>(snapshotSessionKey, undefined);
|
||||||
|
|
||||||
|
const persistGridStateTimeout = useRef<NodeJS.Timeout | undefined>(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import Stack from '@mui/material/Stack';
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { ComponentProps, useCallback, useMemo, useState } from 'react';
|
import { ComponentProps, useCallback, useMemo, useState } from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { ResumeFab } from '@/modules/manga/components/ResumeFAB.tsx';
|
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 { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||||
import { ChapterListCard } from '@/modules/chapter/components/cards/ChapterListCard.tsx';
|
import { ChapterListCard } from '@/modules/chapter/components/cards/ChapterListCard.tsx';
|
||||||
|
import { VirtuosoPersisted } from '@/lib/virtuoso/Component/VirtuosoPersisted.tsx';
|
||||||
|
|
||||||
type ChapterListHeaderProps = {
|
type ChapterListHeaderProps = {
|
||||||
scrollbarWidth: number;
|
scrollbarWidth: number;
|
||||||
@@ -61,7 +61,7 @@ const ChapterListHeader = styled(Stack, {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
type StyledVirtuosoProps = { topOffset: number };
|
type StyledVirtuosoProps = { topOffset: number };
|
||||||
const StyledVirtuoso = styled(Virtuoso, {
|
const StyledVirtuoso = styled(VirtuosoPersisted, {
|
||||||
shouldForwardProp: shouldForwardProp<StyledVirtuosoProps>(['topOffset']),
|
shouldForwardProp: shouldForwardProp<StyledVirtuosoProps>(['topOffset']),
|
||||||
})<StyledVirtuosoProps>(({ theme, topOffset }) => ({
|
})<StyledVirtuosoProps>(({ theme, topOffset }) => ({
|
||||||
listStyle: 'none',
|
listStyle: 'none',
|
||||||
@@ -234,6 +234,7 @@ export const ChapterList = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<StyledVirtuoso
|
<StyledVirtuoso
|
||||||
|
persistKey={`manga-${manga.id}-chapter-list`}
|
||||||
topOffset={appBarHeight + chapterListHeaderHeight}
|
topOffset={appBarHeight + chapterListHeaderHeight}
|
||||||
style={{
|
style={{
|
||||||
// override Virtuoso default values and set them with class
|
// override Virtuoso default values and set them with class
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
* 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 { ContextProp, GroupedVirtuoso, TopItemListProps } from 'react-virtuoso';
|
import { ContextProp, TopItemListProps } from 'react-virtuoso';
|
||||||
import { ComponentProps, useMemo } from 'react';
|
import { ComponentProps, useMemo } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||||
|
import { GroupedVirtuosoPersisted } from '@/lib/virtuoso/Component/GroupedVirtuosoPersisted.tsx';
|
||||||
|
|
||||||
const StickyVirtuosoHeaderWithOffset =
|
const StickyVirtuosoHeaderWithOffset =
|
||||||
(topOffset: number) =>
|
(topOffset: number) =>
|
||||||
@@ -23,7 +24,7 @@ export const StyledGroupedVirtuoso = ({
|
|||||||
heightToSubtract = 0,
|
heightToSubtract = 0,
|
||||||
style,
|
style,
|
||||||
...props
|
...props
|
||||||
}: ComponentProps<typeof GroupedVirtuoso> & { heightToSubtract?: number }) => {
|
}: ComponentProps<typeof GroupedVirtuosoPersisted> & { heightToSubtract?: number }) => {
|
||||||
const { appBarHeight, bottomBarHeight } = useNavBarContext();
|
const { appBarHeight, bottomBarHeight } = useNavBarContext();
|
||||||
|
|
||||||
const TopItemList = useMemo(
|
const TopItemList = useMemo(
|
||||||
@@ -32,7 +33,7 @@ export const StyledGroupedVirtuoso = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GroupedVirtuoso
|
<GroupedVirtuosoPersisted
|
||||||
useWindowScroll
|
useWindowScroll
|
||||||
{...props}
|
{...props}
|
||||||
components={{
|
components={{
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
import DeleteSweepIcon from '@mui/icons-material/DeleteSweep';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
|
||||||
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core';
|
import { closestCenter, DndContext, DragEndEvent } from '@dnd-kit/core';
|
||||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
import { useWindowEvent } from '@mantine/hooks';
|
import { useWindowEvent } from '@mantine/hooks';
|
||||||
@@ -32,6 +31,7 @@ import { DownloadQueueChapterCard } from '@/modules/downloads/components/Downloa
|
|||||||
import { useAppTitle } from '@/modules/navigation-bar/hooks/useAppTitle.ts';
|
import { useAppTitle } from '@/modules/navigation-bar/hooks/useAppTitle.ts';
|
||||||
import { useAppAction } from '@/modules/navigation-bar/hooks/useAppAction.ts';
|
import { useAppAction } from '@/modules/navigation-bar/hooks/useAppAction.ts';
|
||||||
import { ChapterDownloadStatus } from '@/modules/chapter/Chapter.types.ts';
|
import { ChapterDownloadStatus } from '@/modules/chapter/Chapter.types.ts';
|
||||||
|
import { VirtuosoPersisted } from '@/lib/virtuoso/Component/VirtuosoPersisted.tsx';
|
||||||
|
|
||||||
export const DownloadQueue: React.FC = () => {
|
export const DownloadQueue: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -159,7 +159,8 @@ export const DownloadQueue: React.FC = () => {
|
|||||||
onDragAbort={() => setDndActiveDownload(null)}
|
onDragAbort={() => setDndActiveDownload(null)}
|
||||||
>
|
>
|
||||||
<SortableContext items={dndItems} strategy={verticalListSortingStrategy}>
|
<SortableContext items={dndItems} strategy={verticalListSortingStrategy}>
|
||||||
<Virtuoso
|
<VirtuosoPersisted
|
||||||
|
persistKey="download-queue"
|
||||||
useWindowScroll
|
useWindowScroll
|
||||||
overscan={window.innerHeight * 0.5}
|
overscan={window.innerHeight * 0.5}
|
||||||
totalCount={queue.length}
|
totalCount={queue.length}
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledGroupedVirtuoso
|
<StyledGroupedVirtuoso
|
||||||
|
persistKey="extensions"
|
||||||
heightToSubtract={tabsMenuHeight}
|
heightToSubtract={tabsMenuHeight}
|
||||||
overscan={window.innerHeight * 0.5}
|
overscan={window.innerHeight * 0.5}
|
||||||
groupCounts={groupCounts}
|
groupCounts={groupCounts}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export const History: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledGroupedVirtuoso
|
<StyledGroupedVirtuoso
|
||||||
|
persistKey="history"
|
||||||
components={{
|
components={{
|
||||||
Footer: () => (isLoading ? <LoadingPlaceholder usePadding /> : null),
|
Footer: () => (isLoading ? <LoadingPlaceholder usePadding /> : null),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export const LibraryDuplicates = () => {
|
|||||||
if (gridLayout === GridLayout.List) {
|
if (gridLayout === GridLayout.List) {
|
||||||
return (
|
return (
|
||||||
<StyledGroupedVirtuoso
|
<StyledGroupedVirtuoso
|
||||||
|
persistKey="library-duplicates"
|
||||||
groupCounts={mangasCountByTitle}
|
groupCounts={mangasCountByTitle}
|
||||||
groupContent={(index) => (
|
groupContent={(index) => (
|
||||||
<StyledGroupHeader isFirstItem={index === 0}>
|
<StyledGroupHeader isFirstItem={index === 0}>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import React, {
|
|||||||
ForwardedRef,
|
ForwardedRef,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -19,22 +18,20 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import Grid, { GridTypeMap } from '@mui/material/Grid';
|
import Grid, { GridTypeMap } from '@mui/material/Grid';
|
||||||
import Box, { BoxProps } from '@mui/material/Box';
|
import Box, { BoxProps } from '@mui/material/Box';
|
||||||
import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso';
|
import { GridItemProps } from 'react-virtuoso';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||||
import { LoadingPlaceholder } from '@/modules/core/components/feedback/LoadingPlaceholder.tsx';
|
import { LoadingPlaceholder } from '@/modules/core/components/feedback/LoadingPlaceholder.tsx';
|
||||||
import { MangaCard } from '@/modules/manga/components/cards/MangaCard.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 { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
||||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
|
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 { MangaCardProps } from '@/modules/manga/Manga.types.ts';
|
||||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||||
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||||
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
||||||
|
import { VirtuosoGridPersisted } from '@/lib/virtuoso/Component/VirtuosoGridPersisted.tsx';
|
||||||
|
|
||||||
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => (
|
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => (
|
||||||
<Grid {...props} ref={ref} container spacing={1}>
|
<Grid {...props} ref={ref} container spacing={1}>
|
||||||
@@ -138,8 +135,7 @@ const HorizontalGrid = forwardRef(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getGridSnapshotKey = (location: ReturnType<typeof useLocation>) =>
|
export const MANGA_GRID_SNAPSHOT_KEY = 'MangaGrid-snapshot-location';
|
||||||
`MangaGrid-snapshot-location-${location.key}`;
|
|
||||||
|
|
||||||
const VerticalGrid = forwardRef(
|
const VerticalGrid = forwardRef(
|
||||||
(
|
(
|
||||||
@@ -160,69 +156,45 @@ const VerticalGrid = forwardRef(
|
|||||||
loadMore: () => void;
|
loadMore: () => void;
|
||||||
},
|
},
|
||||||
ref: ForwardedRef<HTMLDivElement | null>,
|
ref: ForwardedRef<HTMLDivElement | null>,
|
||||||
) => {
|
) => (
|
||||||
const location = useLocation<{ snapshot?: GridStateSnapshot }>();
|
<>
|
||||||
|
<Box ref={ref}>
|
||||||
const snapshotSessionKey = getGridSnapshotKey(location);
|
<VirtuosoGridPersisted
|
||||||
const [snapshot] = useSessionStorage<GridStateSnapshot | undefined>(snapshotSessionKey, undefined);
|
persistKey={MANGA_GRID_SNAPSHOT_KEY}
|
||||||
|
useWindowScroll
|
||||||
const persistGridStateTimeout = useRef<NodeJS.Timeout | undefined>(undefined);
|
increaseViewportBy={window.innerHeight * 0.5}
|
||||||
const persistGridState = (gridState: GridStateSnapshot) => {
|
totalCount={mangas.length}
|
||||||
const currentUrl = window.location.href;
|
components={{
|
||||||
|
List: GridContainer,
|
||||||
clearTimeout(persistGridStateTimeout.current);
|
Item: GridItemContainer,
|
||||||
persistGridStateTimeout.current = setTimeout(() => {
|
}}
|
||||||
const didLocationChange = currentUrl !== window.location.href;
|
endReached={() => loadMore()}
|
||||||
if (didLocationChange) {
|
computeItemKey={(index) => mangas[index].id}
|
||||||
return;
|
itemContent={(index) =>
|
||||||
}
|
createMangaCard(
|
||||||
|
mangas[index],
|
||||||
AppStorage.session.setItem(snapshotSessionKey, gridState, false);
|
gridLayout,
|
||||||
}, 250);
|
inLibraryIndicator,
|
||||||
};
|
isSelectModeActive,
|
||||||
useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]);
|
selectedMangaIds,
|
||||||
|
handleSelection,
|
||||||
return (
|
mode,
|
||||||
<>
|
)
|
||||||
<Box ref={ref}>
|
}
|
||||||
<VirtuosoGrid
|
/>
|
||||||
useWindowScroll
|
</Box>
|
||||||
increaseViewportBy={window.innerHeight * 0.5}
|
{/* render div to prevent UI jumping around when showing/hiding loading placeholder */
|
||||||
totalCount={mangas.length}
|
/* eslint-disable-next-line no-nested-ternary */}
|
||||||
components={{
|
{isSelectModeActive && gridLayout === GridLayout.List ? (
|
||||||
List: GridContainer,
|
<Box sx={{ paddingBottom: DEFAULT_FULL_FAB_HEIGHT }} />
|
||||||
Item: GridItemContainer,
|
) : // eslint-disable-next-line no-nested-ternary
|
||||||
}}
|
isLoading ? (
|
||||||
restoreStateFrom={snapshot}
|
<LoadingPlaceholder />
|
||||||
stateChanged={persistGridState}
|
) : hasNextPage ? (
|
||||||
endReached={() => loadMore()}
|
<div style={{ height: '75px' }} />
|
||||||
computeItemKey={(index) => mangas[index].id}
|
) : null}
|
||||||
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
|
export interface IMangaGridProps
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ import {
|
|||||||
useMetadataServerSettings,
|
useMetadataServerSettings,
|
||||||
} from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
} from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
||||||
import { useLocalStorage, useSessionStorage } from '@/modules/core/hooks/useStorage.tsx';
|
import { useLocalStorage, useSessionStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||||
import { AppStorage } from '@/lib/storage/AppStorage.ts';
|
import { MANGA_GRID_SNAPSHOT_KEY } from '@/modules/manga/components/MangaGrid.tsx';
|
||||||
import { getGridSnapshotKey } from '@/modules/manga/components/MangaGrid.tsx';
|
|
||||||
import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/modules/source/services/SourceMetadata.ts';
|
import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/modules/source/services/SourceMetadata.ts';
|
||||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||||
import { GET_SOURCE_BROWSE } from '@/lib/graphql/queries/SourceQuery.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 { Sources } from '@/modules/source/services/Sources.ts';
|
||||||
import { useAppTitleAndAction } from '@/modules/navigation-bar/hooks/useAppTitleAndAction.ts';
|
import { useAppTitleAndAction } from '@/modules/navigation-bar/hooks/useAppTitleAndAction.ts';
|
||||||
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||||
|
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
|
||||||
|
|
||||||
const DEFAULT_SOURCE: SourceIdInfo = { id: '-1' };
|
const DEFAULT_SOURCE: SourceIdInfo = { id: '-1' };
|
||||||
|
|
||||||
@@ -246,10 +246,12 @@ export function SourceMangas() {
|
|||||||
query ? SourceContentType.SEARCH : currentContentType!,
|
query ? SourceContentType.SEARCH : currentContentType!,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { key: persistedGridStateKey, deleteState: deletePersistedGridState } =
|
||||||
|
VirtuosoUtil.usePersistState(MANGA_GRID_SNAPSHOT_KEY);
|
||||||
const scrollToTop = useCallback(() => {
|
const scrollToTop = useCallback(() => {
|
||||||
AppStorage.session.setItem(getGridSnapshotKey(location), undefined, false);
|
deletePersistedGridState();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}, [locationKey]);
|
}, [persistedGridStateKey]);
|
||||||
|
|
||||||
const currentQuery = useRef(query);
|
const currentQuery = useRef(query);
|
||||||
const currentAbortRequest = useRef<(reason: any) => void>(() => {});
|
const currentAbortRequest = useRef<(reason: any) => void>(() => {});
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export const Updates: React.FC = () => {
|
|||||||
})}
|
})}
|
||||||
</Typography>
|
</Typography>
|
||||||
<StyledGroupedVirtuoso
|
<StyledGroupedVirtuoso
|
||||||
|
persistKey="updates"
|
||||||
heightToSubtract={lastUpdateTimestampCompHeight}
|
heightToSubtract={lastUpdateTimestampCompHeight}
|
||||||
components={{
|
components={{
|
||||||
Footer: () => (isLoading ? <LoadingPlaceholder usePadding /> : null),
|
Footer: () => (isLoading ? <LoadingPlaceholder usePadding /> : null),
|
||||||
|
|||||||
Reference in New Issue
Block a user