From 09b10cd5abba260741084fad71afdfe976d3e372 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 26 Jun 2023 20:40:35 +0200 Subject: [PATCH] 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 --- src/App.tsx | 2 +- src/components/navbar/DefaultNavBar.tsx | 18 +++++--- src/components/navbar/ReaderNavBar.tsx | 7 ++++ src/screens/Manga.tsx | 3 +- src/screens/settings/About.tsx | 4 +- src/screens/settings/Backup.tsx | 4 +- src/screens/settings/Categories.tsx | 4 +- .../settings/DefaultReaderSettings.tsx | 4 +- src/screens/settings/LibrarySettings.tsx | 4 +- src/screens/settings/SearchSettings.tsx | 3 ++ src/util/useHistory.ts | 42 +++++++++++++++++++ 11 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 src/util/useHistory.ts diff --git a/src/App.tsx b/src/App.tsx index 9b98120d..36b1a176 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -49,7 +49,7 @@ const App: React.FC = () => ( > {/* General Routes */} - } /> + } /> } /> } /> diff --git a/src/components/navbar/DefaultNavBar.tsx b/src/components/navbar/DefaultNavBar.tsx index 11f9cf4b..d04acab3 100644 --- a/src/components/navbar/DefaultNavBar.tsx +++ b/src/components/navbar/DefaultNavBar.tsx @@ -24,13 +24,14 @@ import GetAppIcon from '@mui/icons-material/GetApp'; import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined'; import SettingsIcon from '@mui/icons-material/Settings'; 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 { NavbarItem } from '@/typings'; import NavBarContext from '@/components/context/NavbarContext'; import ExtensionOutlinedIcon from '@/components/util/CustomExtensionOutlinedIcon'; import DesktopSideBar from '@/components/navbar/navigation/DesktopSideBar'; import MobileBottomBar from '@/components/navbar/navigation/MobileBottomBar'; +import { useHistory } from '@/util/useHistory'; const navbarItems: Array = [ { @@ -89,7 +90,8 @@ export default function DefaultNavBar() { const theme = useTheme(); const navigate = useNavigate(); - const { pathname } = useLocation(); + const { pathname, ...location } = useLocation(); + const history = useHistory(); const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm')); const isMainRoute = navbarItems.some(({ path }) => path === pathname); @@ -107,7 +109,14 @@ export default function DefaultNavBar() { } 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); }; @@ -117,8 +126,7 @@ export default function DefaultNavBar() { {!isMainRoute && ( { + 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. // in case this gets changed this has to be updated navigate(-1); diff --git a/src/screens/Manga.tsx b/src/screens/Manga.tsx index 9339d68d..3660c23c 100644 --- a/src/screens/Manga.tsx +++ b/src/screens/Manga.tsx @@ -12,7 +12,7 @@ import React, { useContext, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; 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 { useRefreshManga } from '@/components/manga/hooks'; 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 [refresh, { loading: refreshing }] = useRefreshManga(id); + useSetDefaultBackTo('library'); useEffect(() => { // Automatically fetch manga from source if data is older then 24 hours diff --git a/src/screens/settings/About.tsx b/src/screens/settings/About.tsx index 81c0ec7c..c927e787 100644 --- a/src/screens/settings/About.tsx +++ b/src/screens/settings/About.tsx @@ -13,7 +13,7 @@ import ListItemText from '@mui/material/ListItemText'; import { useTranslation } from 'react-i18next'; import requestManager from '@/lib/RequestManager'; 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'; export default function About() { @@ -27,6 +27,8 @@ export default function About() { const { data: about } = requestManager.useGetAbout(); + useSetDefaultBackTo('settings'); + if (about === undefined) { return ; } diff --git a/src/screens/settings/Backup.tsx b/src/screens/settings/Backup.tsx index fc839eb4..0c4bb205 100644 --- a/src/screens/settings/Backup.tsx +++ b/src/screens/settings/Backup.tsx @@ -15,7 +15,7 @@ import { ListItemButton } from '@mui/material'; import requestManager from '@/lib/RequestManager'; import makeToast from '@/components/util/Toast'; import ListItemLink from '@/components/util/ListItemLink'; -import NavbarContext from '@/components/context/NavbarContext'; +import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext'; export default function Backup() { const { t } = useTranslation(); @@ -25,6 +25,8 @@ export default function Backup() { setAction(null); }, [t]); + useSetDefaultBackTo('settings'); + const submitBackup = (file: File) => { if (file.name.toLowerCase().endsWith('proto.gz')) { makeToast(t('settings.backup.label.restoring_backup'), 'info'); diff --git a/src/screens/settings/Categories.tsx b/src/screens/settings/Categories.tsx index ef8e1c65..edf3c81d 100644 --- a/src/screens/settings/Categories.tsx +++ b/src/screens/settings/Categories.tsx @@ -28,7 +28,7 @@ import { ICategory } from '@/typings'; import requestManager from '@/lib/RequestManager'; import StrictModeDroppable from '@/lib/StrictModeDroppable'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab'; -import NavbarContext from '@/components/context/NavbarContext'; +import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext'; const getItemStyle = ( isDragging: boolean, @@ -67,6 +67,8 @@ export default function Categories() { const [dialogDefault, setDialogDefault] = useState(false); const theme = useTheme(); + useSetDefaultBackTo('settings'); + const categoryReorder = (list: ICategory[], from: number, to: number) => { const newData = [...list]; const [removed] = newData.splice(from, 1); diff --git a/src/screens/settings/DefaultReaderSettings.tsx b/src/screens/settings/DefaultReaderSettings.tsx index 08271991..e54238d5 100644 --- a/src/screens/settings/DefaultReaderSettings.tsx +++ b/src/screens/settings/DefaultReaderSettings.tsx @@ -19,7 +19,7 @@ import { } from '@/util/readerSettings'; import ReaderSettingsOptions from '@/components/reader/ReaderSettingsOptions'; import makeToast from '@/components/util/Toast'; -import NavbarContext from '@/components/context/NavbarContext'; +import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext'; export default function DefaultReaderSettings() { const { t } = useTranslation(); @@ -31,6 +31,8 @@ export default function DefaultReaderSettings() { const { metadata, settings, loading } = useDefaultReaderSettings(); + useSetDefaultBackTo('settings'); + const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => { requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'), diff --git a/src/screens/settings/LibrarySettings.tsx b/src/screens/settings/LibrarySettings.tsx index 38186e02..de5c3fa0 100644 --- a/src/screens/settings/LibrarySettings.tsx +++ b/src/screens/settings/LibrarySettings.tsx @@ -24,7 +24,7 @@ import { ICategory, IncludeInGlobalUpdate } from '@/typings'; import requestManager from '@/lib/RequestManager'; import makeToast from '@/components/util/Toast'; 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'; const CategoriesDiv = styled('div')({ @@ -78,6 +78,8 @@ export default function LibrarySettings() { setAction(null); }, [t]); + useSetDefaultBackTo('settings'); + const { data: categories = [], error: requestError, mutate } = requestManager.useGetCategories(); const [dialogCategories, setDialogCategories] = useState(categories); const [isDialogOpen, setIsDialogOpen] = useState(false); diff --git a/src/screens/settings/SearchSettings.tsx b/src/screens/settings/SearchSettings.tsx index 487726d7..c6bad8b9 100644 --- a/src/screens/settings/SearchSettings.tsx +++ b/src/screens/settings/SearchSettings.tsx @@ -16,11 +16,14 @@ import { SearchMetadataKeys } from '@/typings'; import { requestUpdateServerMetadata } from '@/util/metadata'; import { useSearchSettings } from '@/util/searchSettings'; import makeToast from '@/components/util/Toast'; +import { useSetDefaultBackTo } from '@/components/context/NavbarContext'; export default function SearchSettings() { const { t } = useTranslation(); const { metadata, settings } = useSearchSettings(); + useSetDefaultBackTo('settings'); + const setSettingValue = (key: SearchMetadataKeys, value: boolean) => { requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => makeToast(t('search.error.label.failed_to_save_settings'), 'warning'), diff --git a/src/util/useHistory.ts b/src/util/useHistory.ts new file mode 100644 index 00000000..55307465 --- /dev/null +++ b/src/util/useHistory.ts @@ -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([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; +};