Fix/back button not working without browser history (#389)

* Introduce local history stack

To be able to know if the browser back navigation should be used, it is sometimes necessary to know which the previous page was.
E.g. it should not be used in case the current page is the manga page and the previous one is the reader.

* Handle cases where there is no previous page in the history stack

* Always use a button for the navigation back button

* Navigate from base route to library by replacing the current location

The base route should not be included in the history
This commit is contained in:
schroda
2023-06-26 20:40:35 +02:00
committed by GitHub
parent 1d76e990ae
commit 09b10cd5ab
11 changed files with 83 additions and 12 deletions

View File

@@ -49,7 +49,7 @@ const App: React.FC = () => (
> >
<Routes> <Routes>
{/* General Routes */} {/* General Routes */}
<Route path="/" element={<Navigate to="/library" />} /> <Route path="/" element={<Navigate to="/library" replace />} />
<Route path="settings"> <Route path="settings">
<Route index element={<Settings />} /> <Route index element={<Settings />} />
<Route path="about" element={<About />} /> <Route path="about" element={<About />} />

View File

@@ -24,13 +24,14 @@ import GetAppIcon from '@mui/icons-material/GetApp';
import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined'; import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import ArrowBack from '@mui/icons-material/ArrowBack'; import ArrowBack from '@mui/icons-material/ArrowBack';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { NavbarItem } from '@/typings'; import { NavbarItem } from '@/typings';
import NavBarContext from '@/components/context/NavbarContext'; import NavBarContext from '@/components/context/NavbarContext';
import ExtensionOutlinedIcon from '@/components/util/CustomExtensionOutlinedIcon'; import ExtensionOutlinedIcon from '@/components/util/CustomExtensionOutlinedIcon';
import DesktopSideBar from '@/components/navbar/navigation/DesktopSideBar'; import DesktopSideBar from '@/components/navbar/navigation/DesktopSideBar';
import MobileBottomBar from '@/components/navbar/navigation/MobileBottomBar'; import MobileBottomBar from '@/components/navbar/navigation/MobileBottomBar';
import { useHistory } from '@/util/useHistory';
const navbarItems: Array<NavbarItem> = [ const navbarItems: Array<NavbarItem> = [
{ {
@@ -89,7 +90,8 @@ export default function DefaultNavBar() {
const theme = useTheme(); const theme = useTheme();
const navigate = useNavigate(); const navigate = useNavigate();
const { pathname } = useLocation(); const { pathname, ...location } = useLocation();
const history = useHistory();
const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm')); const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm'));
const isMainRoute = navbarItems.some(({ path }) => path === pathname); const isMainRoute = navbarItems.some(({ path }) => path === pathname);
@@ -107,7 +109,14 @@ export default function DefaultNavBar() {
} }
const handleBack = () => { const handleBack = () => {
if (backToUrl != null) return; const isLastPageInHistory = location.key === 'default';
const wasPreviousPageReader = history[history.length - 2]?.match(/\/manga\/[0-9]+\/chapter\/[0-9]+.*/g);
if (isLastPageInHistory || wasPreviousPageReader) {
navigate(backToUrl ?? '');
return;
}
navigate(-1); navigate(-1);
}; };
@@ -117,8 +126,7 @@ export default function DefaultNavBar() {
<Toolbar> <Toolbar>
{!isMainRoute && ( {!isMainRoute && (
<IconButton <IconButton
component={backToUrl ? Link : 'button'} component="button"
to={backToUrl}
edge="start" edge="start"
sx={{ marginRight: theme.spacing(2) }} sx={{ marginRight: theme.spacing(2) }}
color="inherit" color="inherit"

View File

@@ -186,6 +186,13 @@ export default function ReaderNavBar(props: IProps) {
}, [handleScroll]); // handleScroll changes on every render }, [handleScroll]); // handleScroll changes on every render
const handleClose = () => { const handleClose = () => {
const isLastPageInHistory = location.key === 'default';
if (isLastPageInHistory) {
navigate(`/manga/${manga.id}`);
return;
}
// this works because opening previous/next chapter will replace the current history element. // this works because opening previous/next chapter will replace the current history element.
// in case this gets changed this has to be updated // in case this gets changed this has to be updated
navigate(-1); navigate(-1);

View File

@@ -12,7 +12,7 @@ import React, { useContext, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import requestManager from '@/lib/RequestManager'; import requestManager from '@/lib/RequestManager';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
import ChapterList from '@/components/manga/ChapterList'; import ChapterList from '@/components/manga/ChapterList';
import { useRefreshManga } from '@/components/manga/hooks'; import { useRefreshManga } from '@/components/manga/hooks';
import MangaDetails from '@/components/manga/MangaDetails'; import MangaDetails from '@/components/manga/MangaDetails';
@@ -32,6 +32,7 @@ const Manga: React.FC = () => {
const { data: manga, error, isLoading, isValidating, mutate } = requestManager.useGetManga(id); const { data: manga, error, isLoading, isValidating, mutate } = requestManager.useGetManga(id);
const [refresh, { loading: refreshing }] = useRefreshManga(id); const [refresh, { loading: refreshing }] = useRefreshManga(id);
useSetDefaultBackTo('library');
useEffect(() => { useEffect(() => {
// Automatically fetch manga from source if data is older then 24 hours // Automatically fetch manga from source if data is older then 24 hours

View File

@@ -13,7 +13,7 @@ import ListItemText from '@mui/material/ListItemText';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import requestManager from '@/lib/RequestManager'; import requestManager from '@/lib/RequestManager';
import ListItemLink from '@/components/util/ListItemLink'; import ListItemLink from '@/components/util/ListItemLink';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
import LoadingPlaceholder from '@/components/util/LoadingPlaceholder'; import LoadingPlaceholder from '@/components/util/LoadingPlaceholder';
export default function About() { export default function About() {
@@ -27,6 +27,8 @@ export default function About() {
const { data: about } = requestManager.useGetAbout(); const { data: about } = requestManager.useGetAbout();
useSetDefaultBackTo('settings');
if (about === undefined) { if (about === undefined) {
return <LoadingPlaceholder />; return <LoadingPlaceholder />;
} }

View File

@@ -15,7 +15,7 @@ import { ListItemButton } from '@mui/material';
import requestManager from '@/lib/RequestManager'; import requestManager from '@/lib/RequestManager';
import makeToast from '@/components/util/Toast'; import makeToast from '@/components/util/Toast';
import ListItemLink from '@/components/util/ListItemLink'; import ListItemLink from '@/components/util/ListItemLink';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
export default function Backup() { export default function Backup() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -25,6 +25,8 @@ export default function Backup() {
setAction(null); setAction(null);
}, [t]); }, [t]);
useSetDefaultBackTo('settings');
const submitBackup = (file: File) => { const submitBackup = (file: File) => {
if (file.name.toLowerCase().endsWith('proto.gz')) { if (file.name.toLowerCase().endsWith('proto.gz')) {
makeToast(t('settings.backup.label.restoring_backup'), 'info'); makeToast(t('settings.backup.label.restoring_backup'), 'info');

View File

@@ -28,7 +28,7 @@ import { ICategory } from '@/typings';
import requestManager from '@/lib/RequestManager'; import requestManager from '@/lib/RequestManager';
import StrictModeDroppable from '@/lib/StrictModeDroppable'; import StrictModeDroppable from '@/lib/StrictModeDroppable';
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
const getItemStyle = ( const getItemStyle = (
isDragging: boolean, isDragging: boolean,
@@ -67,6 +67,8 @@ export default function Categories() {
const [dialogDefault, setDialogDefault] = useState<boolean>(false); const [dialogDefault, setDialogDefault] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
useSetDefaultBackTo('settings');
const categoryReorder = (list: ICategory[], from: number, to: number) => { const categoryReorder = (list: ICategory[], from: number, to: number) => {
const newData = [...list]; const newData = [...list];
const [removed] = newData.splice(from, 1); const [removed] = newData.splice(from, 1);

View File

@@ -19,7 +19,7 @@ import {
} from '@/util/readerSettings'; } from '@/util/readerSettings';
import ReaderSettingsOptions from '@/components/reader/ReaderSettingsOptions'; import ReaderSettingsOptions from '@/components/reader/ReaderSettingsOptions';
import makeToast from '@/components/util/Toast'; import makeToast from '@/components/util/Toast';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
export default function DefaultReaderSettings() { export default function DefaultReaderSettings() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -31,6 +31,8 @@ export default function DefaultReaderSettings() {
const { metadata, settings, loading } = useDefaultReaderSettings(); const { metadata, settings, loading } = useDefaultReaderSettings();
useSetDefaultBackTo('settings');
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => { const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'), makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),

View File

@@ -24,7 +24,7 @@ import { ICategory, IncludeInGlobalUpdate } from '@/typings';
import requestManager from '@/lib/RequestManager'; import requestManager from '@/lib/RequestManager';
import makeToast from '@/components/util/Toast'; import makeToast from '@/components/util/Toast';
import ThreeStateCheckboxInput from '@/components/atoms/ThreeStateCheckboxInput'; import ThreeStateCheckboxInput from '@/components/atoms/ThreeStateCheckboxInput';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
import SearchSettings from '@/screens/settings/SearchSettings'; import SearchSettings from '@/screens/settings/SearchSettings';
const CategoriesDiv = styled('div')({ const CategoriesDiv = styled('div')({
@@ -78,6 +78,8 @@ export default function LibrarySettings() {
setAction(null); setAction(null);
}, [t]); }, [t]);
useSetDefaultBackTo('settings');
const { data: categories = [], error: requestError, mutate } = requestManager.useGetCategories(); const { data: categories = [], error: requestError, mutate } = requestManager.useGetCategories();
const [dialogCategories, setDialogCategories] = useState<ICategory[]>(categories); const [dialogCategories, setDialogCategories] = useState<ICategory[]>(categories);
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);

View File

@@ -16,11 +16,14 @@ import { SearchMetadataKeys } from '@/typings';
import { requestUpdateServerMetadata } from '@/util/metadata'; import { requestUpdateServerMetadata } from '@/util/metadata';
import { useSearchSettings } from '@/util/searchSettings'; import { useSearchSettings } from '@/util/searchSettings';
import makeToast from '@/components/util/Toast'; import makeToast from '@/components/util/Toast';
import { useSetDefaultBackTo } from '@/components/context/NavbarContext';
export default function SearchSettings() { export default function SearchSettings() {
const { t } = useTranslation(); const { t } = useTranslation();
const { metadata, settings } = useSearchSettings(); const { metadata, settings } = useSearchSettings();
useSetDefaultBackTo('settings');
const setSettingValue = (key: SearchMetadataKeys, value: boolean) => { const setSettingValue = (key: SearchMetadataKeys, value: boolean) => {
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
makeToast(t('search.error.label.failed_to_save_settings'), 'warning'), makeToast(t('search.error.label.failed_to_save_settings'), 'warning'),

42
src/util/useHistory.ts Normal file
View File

@@ -0,0 +1,42 @@
/*
* 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 { useEffect, useState } from 'react';
import { NavigationType, useLocation, useNavigationType } from 'react-router-dom';
// eslint-disable-next-line import/prefer-default-export
export const useHistory = () => {
const location = useLocation();
const navigationType = useNavigationType();
const [history, setHistory] = useState<string[]>([location.pathname]);
useEffect(() => {
const isLastPageInHistory = location.key === 'default';
const ignoreInitialPop = isLastPageInHistory && history.length === 1;
if (ignoreInitialPop) {
return;
}
switch (navigationType) {
case NavigationType.Pop:
setHistory([...history.slice(0, -1)]);
break;
case NavigationType.Push:
setHistory([...history, location.pathname]);
break;
case NavigationType.Replace:
setHistory([...history.slice(0, -1), location.pathname]);
break;
default:
throw new Error(`Unexpected NavigationType "${navigationType}"`);
}
}, [location]);
return history;
};