Feature/improve restoring grid snapshots (#713)

* Update local storage functions

* Update local storage hooks

* Add session storage

* Improve storing virtuoso grid snapshot

* Use "useSyncExternalStore" for browser storage access

* Get rid of location replacements in "AppbarSearch"

Use session storage instead to prevent reloading the page all the time due to changing the location

* Get rid of location replacements in "SourceMangas"

Use session storage instead to prevent reloading the page all the time due to changing the location
This commit is contained in:
schroda
2024-04-07 15:22:03 +02:00
committed by GitHub
parent ea2f9dc5a4
commit aab3e25031
20 changed files with 220 additions and 190 deletions

View File

@@ -10,15 +10,16 @@ import React, { ForwardedRef, forwardRef, useEffect, useLayoutEffect, useMemo, u
import Grid, { GridTypeMap } from '@mui/material/Grid';
import { Box, Typography } from '@mui/material';
import { GridItemProps, GridStateSnapshot, VirtuosoGrid } from 'react-virtuoso';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { EmptyView } from '@/components/util/EmptyView';
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder';
import { MangaCard, MangaCardProps } from '@/components/MangaCard';
import { GridLayout } from '@/components/context/LibraryOptionsContext';
import { useLocalStorage } from '@/util/useLocalStorage';
import { useLocalStorage, useSessionStorage } from '@/util/useStorage.tsx';
import { TManga, TPartialManga } from '@/typings.ts';
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab.tsx';
import { AppStorage } from '@/util/AppStorage.ts';
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => (
<Grid {...props} ref={ref} container sx={{ paddingLeft: '5px', paddingRight: '13px' }}>
@@ -144,8 +145,9 @@ const VerticalGrid = forwardRef(
ref: ForwardedRef<HTMLDivElement | null>,
) => {
const location = useLocation<{ snapshot?: GridStateSnapshot }>();
const navigate = useNavigate();
const { snapshot } = location.state ?? {};
const snapshotSessionKey = `MangaGrid-snapshot-location-${location.key}`;
const [snapshot] = useSessionStorage<GridStateSnapshot | undefined>(snapshotSessionKey, undefined);
const persistGridStateTimeout = useRef<NodeJS.Timeout | undefined>();
const persistGridState = (gridState: GridStateSnapshot) => {
@@ -158,10 +160,7 @@ const VerticalGrid = forwardRef(
return;
}
navigate(
{ pathname: '', search: location.search },
{ replace: true, state: { ...location.state, snapshot: gridState } },
);
AppStorage.session.setItem(snapshotSessionKey, gridState);
}, 250);
};
useEffect(() => clearTimeout(persistGridStateTimeout.current), [location.key, persistGridStateTimeout.current]);

View File

@@ -15,7 +15,7 @@ import {
TChapter,
TranslationKey,
} from '@/typings.ts';
import { useReducerLocalStorage } from '@/util/useLocalStorage.tsx';
import { useReducerLocalStorage } from '@/util/useStorage.tsx';
const defaultChapterOptions: ChapterListOptions = {
active: false,

View File

@@ -12,7 +12,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import { createTheme } from '@/theme';
import { useLocalStorage } from '@/util/useLocalStorage';
import { useLocalStorage } from '@/util/useStorage.tsx';
import { DarkTheme } from '@/components/context/DarkTheme';
import { NavBarContextProvider } from '@/components/navbar/NavBarContextProvider';
import { LibraryOptionsContextProvider } from '@/components/library/LibraryOptionsProvider';

View File

@@ -8,7 +8,7 @@
import React, { useMemo } from 'react';
import { LibraryOptions } from '@/typings';
import { useLocalStorage } from '@/util/useLocalStorage';
import { useLocalStorage } from '@/util/useStorage.tsx';
import { LibraryOptionsContext, DefaultLibraryOptions } from '@/components/context/LibraryOptionsContext';
interface IProps {

View File

@@ -16,7 +16,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { getVersion } from '@/screens/settings/About.tsx';
import { useLocalStorage } from '@/util/useLocalStorage.tsx';
import { useLocalStorage } from '@/util/useStorage.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
const UPDATE_CHECK_INTERVAL = 1000 * 60 * 60 * 24; // 1 day

View File

@@ -11,35 +11,44 @@ import SearchIcon from '@mui/icons-material/Search';
import { IconButton, Tooltip } from '@mui/material';
import { useQueryParam, StringParam } from 'use-query-params';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { SearchTextField } from '@/components/atoms/SearchTextField.tsx';
import { useSessionStorage } from '@/util/useStorage.tsx';
interface IProps {
autoOpen?: boolean;
isClosable?: boolean;
}
const defaultProps = {
autoOpen: false,
};
export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
const { autoOpen } = props;
const { isClosable = true } = props;
const { t } = useTranslation();
const { pathname, search: locationSearch, state: fullLocationState } = useLocation<{ wasSearchOpen?: boolean }>();
const { wasSearchOpen, ...locationState } = fullLocationState ?? {};
const navigate = useNavigate();
const [prevLocationKey, setPrevLocationKey] = useState<string>();
const location = useLocation();
const [query, setQuery] = useQueryParam('query', StringParam);
const [searchOpen, setSearchOpen] = useState(!!query);
const [isSearchOpen, setIsSearchOpen] = useState(!isClosable || !!query);
const inputRef = React.useRef<HTMLInputElement>();
const [searchString, setSearchString] = useState(query ?? '');
const [locationQuery, setLocationQuery] = useSessionStorage<string | null>(`appbarsearch-location-${location.key}`);
if (prevLocationKey !== location.key) {
setPrevLocationKey(location.key);
setLocationQuery(query);
setSearchString(locationQuery ?? '');
setIsSearchOpen(!isClosable || !!locationQuery);
}
const isOpen = isSearchOpen || !!locationQuery;
const updateSearchOpenState = (open: boolean) => {
setSearchOpen(open);
if (!isClosable) {
return;
}
setIsSearchOpen(open);
// try to focus input component since in case of navigating to the previous/next page in the browser history
// the "openSearch" state might not change and thus, won't trigger a focus
@@ -54,6 +63,7 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
}
setQuery(newQuery);
updateSearchOpenState(false);
}
const cancelSearch = () => {
@@ -72,46 +82,6 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
}
};
useEffect(() => {
if ((autoOpen && wasSearchOpen === undefined) || (wasSearchOpen && query)) {
updateSearchOpenState(true);
return;
}
updateSearchOpenState(false);
}, [autoOpen, pathname]);
useEffect(() => {
if (!searchOpen || !inputRef.current) {
return;
}
inputRef.current.focus();
}, [searchOpen, inputRef.current]);
useEffect(() => {
if (wasSearchOpen === searchOpen) {
return;
}
navigate(
{ pathname, search: locationSearch },
{ replace: true, state: { ...locationState, wasSearchOpen: searchOpen } },
);
}, [searchOpen]);
useEffect(() => {
if (query === undefined && searchString !== undefined) {
setSearchString('');
return;
}
if (query && searchString !== query) {
setSearchString(query);
updateSearchOpenState(true);
}
}, [query]);
useEffect(() => {
window.addEventListener('keydown', handleKeyboardEvent);
@@ -120,9 +90,10 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
};
}, [handleKeyboardEvent]);
if (searchOpen) {
if (isOpen) {
return (
<SearchTextField
autoFocus
variant="standard"
value={searchString}
onCancel={cancelSearch}
@@ -146,5 +117,3 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
</Tooltip>
);
};
AppbarSearch.defaultProps = defaultProps;