Restore virtuoso states on browser back navigation

- manga chapter list
- library duplicates
- updates
- history
- download queue
This commit is contained in:
schroda
2025-05-25 15:36:25 +02:00
parent 5f982e07e0
commit c3b22f92a9
13 changed files with 209 additions and 82 deletions

View 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}
/>
);
};

View 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} />;
};

View 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}
/>
);
};

View File

@@ -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<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,
};
}
}