Files
suwayomi-material-you-webui/src/lib/virtuoso/Virtuoso.util.tsx
schroda c3b22f92a9 Restore virtuoso states on browser back navigation
- manga chapter list
- library duplicates
- updates
- history
- download queue
2025-05-25 16:09:53 +02:00

161 lines
5.7 KiB
TypeScript

/*
* 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 { 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;
static readonly ITEMS = 1;
/**
* Returns the index converted to the index of the list of group or normal items.
*
* @example
*
* index 1
* groupCounts [1, 3, 1]
*
* (GCS) groups content size 2 3 1
* (SOG) size of group (with header item) 3 7 9
*
* is group header item: SOG - index === GCS + 1
* is normal item : SOG - index < GCS + 1
*
* converted index:
* is group header
* ? group index
* : index - (group index + 1)
*
* group index 0
* 3 - 0 = 3 (group - index: 0)
* 3 - 1 = 2 (normal - index: 0 = 1 - (0 + 1))
* 3 - 2 = 1 (normal - index: 1 = 2 - (0 + 1))
*
* group index 1
* 7 - 3 = 4 (group - index: 1)
* 7 - 4 = 3 (normal - index: 2 = 4 - (1 + 1))
* 7 - 5 = 2 (normal - index: 3 = 5 - (1 + 1))
* 7 - 6 = 1 (normal - index: 4 = 6 - (1 + 1))
*
* group index 2
* 9 - 7 = 2 (group - index: 2)
* 9 - 8 = 1 (normal - index: 5 = 8 - (2 + 1))
*/
static convertIndex(
index: number,
groupCounts: number[],
sizeOfGroups: number[],
): { type: 'normal' | 'group'; index: number; groupIndex: number } {
for (let groupIndex = 0; groupIndex < groupCounts.length; groupIndex++) {
const groupCount = groupCounts[groupIndex];
const sizeOfGroup = sizeOfGroups[groupIndex];
const isIndexOfGroup = index <= sizeOfGroup - 1;
if (isIndexOfGroup) {
const isGroupHeaderItem = sizeOfGroup - index === groupCount + 1;
return {
type: isGroupHeaderItem ? 'group' : 'normal',
index: isGroupHeaderItem ? groupIndex : index - (groupIndex + 1),
groupIndex,
};
}
}
throw new Error(`Unexpected "${index}" (${index}) and "groupCounts" (${groupCounts})`);
}
static useCreateConvertIndex(
groupCounts: number[],
): (index: number) => ReturnType<typeof VirtuosoUtil.convertIndex> {
const sizeOfGroups = useMemo(() => {
const tmpSizeOfGroups: number[] = [];
for (let i = 0; i < groupCounts.length; i++) {
const maxIndexOfPreviousGroup = tmpSizeOfGroups[i - 1] ?? 0;
tmpSizeOfGroups[i] = groupCounts[i] + 1 + maxIndexOfPreviousGroup;
}
return tmpSizeOfGroups;
}, [groupCounts]);
return useCallback((index: number) => this.convertIndex(index, groupCounts, sizeOfGroups), [sizeOfGroups]);
}
static useCreateGroupedComputeItemKey(
groupCounts: number[],
getGroupKey: (index: number, groupIndex: number) => React.Key,
getNormalKey: (index: number, groupIndex: number) => React.Key,
): (index: number) => React.Key {
const convertIndex = this.useCreateConvertIndex(groupCounts);
return useCallback(
(index) => {
const { type, index: convertedIndex, groupIndex } = convertIndex(index);
switch (type) {
case 'group':
return getGroupKey(convertedIndex, groupIndex);
case 'normal':
return getNormalKey(convertedIndex, groupIndex);
default:
throw new Error(
`VirtuosoUtil::useCreateGroupedComputeItemKey: unexpected "converted index type" (${type})`,
);
}
},
[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,
};
}
}