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:
@@ -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]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user