Fix navbar back button behavior (#195)
* Enable passing backLink through location state. Explicitly handle cases for BACK * Allow pages to override default backLink. Fix manga page back link * Cleanup
This commit is contained in:
@@ -15,6 +15,7 @@ import useLocalStorage from 'util/useLocalStorage';
|
|||||||
import SpinnerImage from 'components/util/SpinnerImage';
|
import SpinnerImage from 'components/util/SpinnerImage';
|
||||||
import { Box, styled } from '@mui/system';
|
import { Box, styled } from '@mui/system';
|
||||||
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||||
|
import { BACK } from 'util/useBackTo';
|
||||||
|
|
||||||
const BottomGradient = styled('div')({
|
const BottomGradient = styled('div')({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -88,12 +89,14 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
const [useCache] = useLocalStorage<boolean>('useCache', true);
|
||||||
const [ItemWidth] = useLocalStorage<number>('ItemWidth', 300);
|
const [ItemWidth] = useLocalStorage<number>('ItemWidth', 300);
|
||||||
|
|
||||||
|
const mangaLinkTo = { pathname: `/manga/${id}/`, state: { backLink: BACK } };
|
||||||
|
|
||||||
if (gridLayout !== 2) {
|
if (gridLayout !== 2) {
|
||||||
const colomns = Math.round(dimensions / ItemWidth);
|
const colomns = Math.round(dimensions / ItemWidth);
|
||||||
return (
|
return (
|
||||||
// @ts-ignore gridsize type isnt allowed to be a decimal but it works fine
|
// @ts-ignore gridsize type isnt allowed to be a decimal but it works fine
|
||||||
<Grid item xs={12 / colomns} sm={12 / colomns} md={12 / colomns} lg={12 / colomns}>
|
<Grid item xs={12 / colomns} sm={12 / colomns} md={12 / colomns} lg={12 / colomns}>
|
||||||
<Link to={`/manga/${id}/`} style={(gridLayout === 1) ? { textDecoration: 'none' } : {}}>
|
<Link to={mangaLinkTo} style={(gridLayout === 1) ? { textDecoration: 'none' } : {}}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -190,7 +193,7 @@ const MangaCard = React.forwardRef<HTMLDivElement, IProps>((props: IProps, ref)
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12} sm={12} md={12} lg={12}>
|
<Grid item xs={12} sm={12} md={12} lg={12}>
|
||||||
<Link to={`/manga/${id}/`} style={{ textDecoration: 'none', color: 'unset' }}>
|
<Link to={mangaLinkTo} style={{ textDecoration: 'none', color: 'unset' }}>
|
||||||
<CardContent sx={{
|
<CardContent sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|||||||
@@ -5,9 +5,13 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* 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/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
|
// Default back button url
|
||||||
|
defaultBackTo: string | undefined
|
||||||
|
setDefaultBackTo: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||||
|
|
||||||
// AppBar title
|
// AppBar title
|
||||||
title: string
|
title: string
|
||||||
setTitle: React.Dispatch<React.SetStateAction<string>>
|
setTitle: React.Dispatch<React.SetStateAction<string>>
|
||||||
@@ -22,6 +26,8 @@ type ContextType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NavBarContext = React.createContext<ContextType>({
|
const NavBarContext = React.createContext<ContextType>({
|
||||||
|
defaultBackTo: undefined,
|
||||||
|
setDefaultBackTo: ():void => {},
|
||||||
title: 'Tachidesk',
|
title: 'Tachidesk',
|
||||||
setTitle: ():void => {},
|
setTitle: ():void => {},
|
||||||
action: <div />,
|
action: <div />,
|
||||||
@@ -31,3 +37,14 @@ const NavBarContext = React.createContext<ContextType>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default NavBarContext;
|
export default NavBarContext;
|
||||||
|
|
||||||
|
export const useNavBarContext = () => useContext(NavBarContext);
|
||||||
|
|
||||||
|
export const useSetDefaultBackTo = (value: string) => {
|
||||||
|
const { setDefaultBackTo } = useNavBarContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDefaultBackTo(value);
|
||||||
|
return () => setDefaultBackTo(undefined);
|
||||||
|
}, [value]);
|
||||||
|
};
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import Typography from '@mui/material/Typography';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import client from 'util/client';
|
import client from 'util/client';
|
||||||
|
import { BACK } from 'util/useBackTo';
|
||||||
|
|
||||||
interface IProps{
|
interface IProps{
|
||||||
chapter: IChapter
|
chapter: IChapter
|
||||||
@@ -118,7 +119,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
|
to={{ pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`, state: { backLink: BACK } }}
|
||||||
style={{
|
style={{
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: theme.palette.text[chapter.read ? 'disabled' : 'primary'],
|
color: theme.palette.text[chapter.read ? 'disabled' : 'primary'],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import React from 'react';
|
|||||||
import { Fab } from '@mui/material';
|
import { Fab } from '@mui/material';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { PlayArrow } from '@mui/icons-material';
|
import { PlayArrow } from '@mui/icons-material';
|
||||||
|
import { BACK } from 'util/useBackTo';
|
||||||
|
|
||||||
interface ResumeFABProps{
|
interface ResumeFABProps{
|
||||||
chapter: IChapter
|
chapter: IChapter
|
||||||
@@ -23,7 +24,7 @@ export default function ResumeFab(props: ResumeFABProps) {
|
|||||||
component={Link}
|
component={Link}
|
||||||
variant="extended"
|
variant="extended"
|
||||||
color="primary"
|
color="primary"
|
||||||
to={`/manga/${mangaId}/chapter/${index}/page/${lastPageRead}`}
|
to={{ pathname: `/manga/${mangaId}/chapter/${index}/page/${lastPageRead}`, state: { backLink: BACK } }}
|
||||||
>
|
>
|
||||||
<PlayArrow />
|
<PlayArrow />
|
||||||
{index === 1 ? 'Start' : 'Resume' }
|
{index === 1 ? 'Start' : 'Resume' }
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ 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 { useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import NavBarContext from 'components/context/NavbarContext';
|
import NavBarContext from 'components/context/NavbarContext';
|
||||||
import DarkTheme from 'components/context/DarkTheme';
|
import DarkTheme from 'components/context/DarkTheme';
|
||||||
import ExtensionOutlinedIcon from 'components/util/CustomExtensionOutlinedIcon';
|
import ExtensionOutlinedIcon from 'components/util/CustomExtensionOutlinedIcon';
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import useBackTo from 'util/useBackTo';
|
||||||
import DesktopSideBar from './navigation/DesktopSideBar';
|
import DesktopSideBar from './navigation/DesktopSideBar';
|
||||||
import MobileBottomBar from './navigation/MobileBottomBar';
|
import MobileBottomBar from './navigation/MobileBottomBar';
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ const navbarItems: Array<NavbarItem> = [
|
|||||||
|
|
||||||
export default function DefaultNavBar() {
|
export default function DefaultNavBar() {
|
||||||
const { title, action, override } = useContext(NavBarContext);
|
const { title, action, override } = useContext(NavBarContext);
|
||||||
|
const backTo = useBackTo();
|
||||||
const { darkTheme } = useContext(DarkTheme);
|
const { darkTheme } = useContext(DarkTheme);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -100,28 +102,30 @@ export default function DefaultNavBar() {
|
|||||||
navbar = <DesktopSideBar navBarItems={navbarItems.filter((it) => it.show !== 'mobile')} />;
|
navbar = <DesktopSideBar navBarItems={navbarItems.filter((it) => it.show !== 'mobile')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (backTo.url != null) return;
|
||||||
|
history.goBack();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<AppBar position="fixed" color={darkTheme ? 'default' : 'primary'}>
|
<AppBar position="fixed" color={darkTheme ? 'default' : 'primary'}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
{
|
{!isMainRoute && (
|
||||||
!navbarItems.some(({ path }) => path === history.location.pathname)
|
<IconButton
|
||||||
&& (
|
component={backTo.url ? Link : 'button'}
|
||||||
<IconButton
|
to={backTo.url}
|
||||||
edge="start"
|
edge="start"
|
||||||
sx={{ marginRight: theme.spacing(2) }}
|
sx={{ marginRight: theme.spacing(2) }}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="menu"
|
aria-label="menu"
|
||||||
disableRipple
|
disableRipple
|
||||||
// when page is opened in new tab backbutton will
|
size="large"
|
||||||
// take you to the library
|
onClick={handleBack}
|
||||||
onClick={() => (history.length === 1 ? history.push('/library') : history.goBack())}
|
>
|
||||||
size="large"
|
<ArrowBack />
|
||||||
>
|
</IconButton>
|
||||||
<ArrowBack />
|
)}
|
||||||
</IconButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<Typography variant={isMobileWidth ? 'h6' : 'h5'} sx={{ flexGrow: 1 }} noWrap textOverflow="ellipsis">
|
<Typography variant={isMobileWidth ? 'h6' : 'h5'} sx={{ flexGrow: 1 }} noWrap textOverflow="ellipsis">
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface IProps{
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function NavBarProvider({ children }:IProps) {
|
export default function NavBarProvider({ children }:IProps) {
|
||||||
|
const [defaultBackTo, setDefaultBackTo] = useState<string | undefined>();
|
||||||
const [title, setTitle] = useState<string>('Tachidesk');
|
const [title, setTitle] = useState<string>('Tachidesk');
|
||||||
const [action, setAction] = useState<any>(<div />);
|
const [action, setAction] = useState<any>(<div />);
|
||||||
const [override, setOverride] = useState<INavbarOverride>({
|
const [override, setOverride] = useState<INavbarOverride>({
|
||||||
@@ -21,6 +22,8 @@ export default function NavBarProvider({ children }:IProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
|
defaultBackTo,
|
||||||
|
setDefaultBackTo,
|
||||||
title,
|
title,
|
||||||
setTitle,
|
setTitle,
|
||||||
action,
|
action,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
|
|||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import { styled } from '@mui/system';
|
import { styled } from '@mui/system';
|
||||||
|
import useBackTo from 'util/useBackTo';
|
||||||
|
|
||||||
const Root = styled('div')(({ theme }) => ({
|
const Root = styled('div')(({ theme }) => ({
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -129,6 +130,7 @@ interface IProps {
|
|||||||
|
|
||||||
export default function ReaderNavBar(props: IProps) {
|
export default function ReaderNavBar(props: IProps) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const backTo = useBackTo();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
settings, setSettings, manga, chapter, curPage,
|
settings, setSettings, manga, chapter, curPage,
|
||||||
@@ -167,6 +169,12 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
};
|
};
|
||||||
}, [handleScroll]);// handleScroll changes on every render
|
}, [handleScroll]);// handleScroll changes on every render
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (backTo.back) history.goBack();
|
||||||
|
else if (backTo.url) history.push(backTo.url);
|
||||||
|
else history.push(`/manga/${manga.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Slide
|
<Slide
|
||||||
@@ -203,7 +211,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="menu"
|
aria-label="menu"
|
||||||
disableRipple
|
disableRipple
|
||||||
onClick={() => history.push('..')}
|
onClick={handleClose}
|
||||||
size="large"
|
size="large"
|
||||||
sx={{ mr: -1 }}
|
sx={{ mr: -1 }}
|
||||||
>
|
>
|
||||||
@@ -315,7 +323,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
&& (
|
&& (
|
||||||
<Link
|
<Link
|
||||||
replace
|
replace
|
||||||
to={`/manga/${manga.id}/chapter/${chapter.index - 1}`}
|
to={{ pathname: `/manga/${manga.id}/chapter/${chapter.index - 1}`, state: history.location.state }}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -331,7 +339,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
<Link
|
<Link
|
||||||
replace
|
replace
|
||||||
style={{ gridArea: 'next' }}
|
style={{ gridArea: 'next' }}
|
||||||
to={`/manga/${manga.id}/chapter/${chapter.index + 1}`}
|
to={{ pathname: `/manga/${manga.id}/chapter/${chapter.index + 1}`, state: history.location.state }}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
CircularProgress, IconButton, Stack, Tooltip,
|
CircularProgress, IconButton, Stack, Tooltip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
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';
|
||||||
@@ -37,6 +37,8 @@ const Manga: React.FC = () => {
|
|||||||
|
|
||||||
const [refresh, { loading: refreshing }] = useRefreshManga(id);
|
const [refresh, { loading: refreshing }] = useRefreshManga(id);
|
||||||
|
|
||||||
|
useSetDefaultBackTo(manga?.inLibrary === false && manga.sourceId != null ? `/sources/${manga.sourceId}/popular` : '/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
|
||||||
// Automatic fetch is done only once, to prevent issues when server does
|
// Automatic fetch is done only once, to prevent issues when server does
|
||||||
|
|||||||
@@ -157,13 +157,13 @@ export default function Reader() {
|
|||||||
formData.append('read', 'true');
|
formData.append('read', 'true');
|
||||||
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
|
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
|
||||||
|
|
||||||
history.replace(`/manga/${manga.id}/chapter/${chapter.index + 1}`);
|
history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index + 1}`, state: history.location.state });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevChapter = () => {
|
const prevChapter = () => {
|
||||||
if (chapter.index > 1) {
|
if (chapter.index > 1) {
|
||||||
history.replace(`/manga/${manga.id}/chapter/${chapter.index - 1}`);
|
history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index - 1}`, state: history.location.state });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export default function Updates() {
|
|||||||
transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
|
transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onClick={() => history.push(`/manga/${chapter.mangaId}/chapter/${chapter.index}`)}
|
onClick={() => history.push({ pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`, state: history.location.state })}
|
||||||
>
|
>
|
||||||
<CardContent sx={{
|
<CardContent sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
24
src/util/useBackTo.ts
Normal file
24
src/util/useBackTo.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useNavBarContext } from 'components/context/NavbarContext';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const BACK = '__BACK__';
|
||||||
|
|
||||||
|
const useBackTo = (): { url?: string, back: boolean } => {
|
||||||
|
const location = useLocation<{ backLink?: string }>();
|
||||||
|
const { defaultBackTo } = useNavBarContext();
|
||||||
|
|
||||||
|
const url = location.state?.backLink ?? defaultBackTo;
|
||||||
|
return {
|
||||||
|
url: url === BACK ? undefined : url,
|
||||||
|
back: url === BACK,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useBackTo;
|
||||||
Reference in New Issue
Block a user