Delete deprecated reader files
This commit is contained in:
@@ -26,7 +26,6 @@ const { Browse } = loadable(() => import('@/modules/browse/screens/Browse.tsx'),
|
|||||||
const { DownloadQueue } = loadable(() => import('@/modules/downloads/screens/DownloadQueue.tsx'), lazyLoadFallback);
|
const { DownloadQueue } = loadable(() => import('@/modules/downloads/screens/DownloadQueue.tsx'), lazyLoadFallback);
|
||||||
const { Library } = loadable(() => import('@/modules/library/screens/Library.tsx'), lazyLoadFallback);
|
const { Library } = loadable(() => import('@/modules/library/screens/Library.tsx'), lazyLoadFallback);
|
||||||
const { Manga } = loadable(() => import('@/modules/manga/screens/Manga.tsx'), lazyLoadFallback);
|
const { Manga } = loadable(() => import('@/modules/manga/screens/Manga.tsx'), lazyLoadFallback);
|
||||||
const { Reader } = loadable(() => import('@/modules/reader/screens/Reader.tsx'), lazyLoadFallback);
|
|
||||||
const { SearchAll } = loadable(() => import('@/modules/global-search/screens/SearchAll.tsx'), lazyLoadFallback);
|
const { SearchAll } = loadable(() => import('@/modules/global-search/screens/SearchAll.tsx'), lazyLoadFallback);
|
||||||
const { Settings } = loadable(() => import('@/modules/settings/screens/Settings.tsx'), lazyLoadFallback);
|
const { Settings } = loadable(() => import('@/modules/settings/screens/Settings.tsx'), lazyLoadFallback);
|
||||||
const { About } = loadable(() => import('@/modules/settings/screens/About.tsx'), lazyLoadFallback);
|
const { About } = loadable(() => import('@/modules/settings/screens/About.tsx'), lazyLoadFallback);
|
||||||
@@ -35,10 +34,6 @@ const { CategorySettings } = loadable(
|
|||||||
() => import('@/modules/category/screens/CategorySettings.tsx'),
|
() => import('@/modules/category/screens/CategorySettings.tsx'),
|
||||||
lazyLoadFallback,
|
lazyLoadFallback,
|
||||||
);
|
);
|
||||||
const { DefaultReaderSettings } = loadable(
|
|
||||||
() => import('@/modules/reader/screens/DefaultReaderSettings.tsx'),
|
|
||||||
lazyLoadFallback,
|
|
||||||
);
|
|
||||||
const { SourceConfigure } = loadable(() => import('@/modules/source/screens/SourceConfigure.tsx'), lazyLoadFallback);
|
const { SourceConfigure } = loadable(() => import('@/modules/source/screens/SourceConfigure.tsx'), lazyLoadFallback);
|
||||||
const { SourceMangas } = loadable(() => import('@/modules/source/screens/SourceMangas.tsx'), lazyLoadFallback);
|
const { SourceMangas } = loadable(() => import('@/modules/source/screens/SourceMangas.tsx'), lazyLoadFallback);
|
||||||
const { Updates } = loadable(() => import('@/modules/updates/screens/Updates.tsx'), lazyLoadFallback);
|
const { Updates } = loadable(() => import('@/modules/updates/screens/Updates.tsx'), lazyLoadFallback);
|
||||||
@@ -119,7 +114,7 @@ const MainApp = () => {
|
|||||||
<Route index element={<Settings />} />
|
<Route index element={<Settings />} />
|
||||||
<Route path="about" element={<About />} />
|
<Route path="about" element={<About />} />
|
||||||
<Route path="categories" element={<CategorySettings />} />
|
<Route path="categories" element={<CategorySettings />} />
|
||||||
<Route path="defaultReaderSettings" element={<DefaultReaderSettings />} />
|
<Route path="defaultReaderSettings" element={null} />
|
||||||
<Route path="librarySettings">
|
<Route path="librarySettings">
|
||||||
<Route index element={<LibrarySettings />} />
|
<Route index element={<LibrarySettings />} />
|
||||||
<Route path="duplicates" element={<LibraryDuplicates />} />
|
<Route path="duplicates" element={<LibraryDuplicates />} />
|
||||||
@@ -164,7 +159,7 @@ const MainApp = () => {
|
|||||||
const ReaderApp = () => (
|
const ReaderApp = () => (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="manga/:mangaId/chapter/:chapterIndex" element={<Reader />} />
|
<Route path="manga/:mangaId/chapter/:chapterIndex" element={null} />
|
||||||
<Route path="*" element={null} />
|
<Route path="*" element={null} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSe
|
|||||||
import { useManageMangaLibraryState } from '@/modules/manga/hooks/useManageMangaLibraryState.tsx';
|
import { useManageMangaLibraryState } from '@/modules/manga/hooks/useManageMangaLibraryState.tsx';
|
||||||
import { ChapterType, MangaReaderFieldsFragment, MangaType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
import { ChapterType, MangaReaderFieldsFragment, MangaType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx';
|
import { SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx';
|
||||||
import { IReaderSettings } from '@/modules/reader/Reader.types';
|
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
||||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||||
|
|
||||||
export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate';
|
export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate';
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
|||||||
skipDupChapters: undefined,
|
skipDupChapters: undefined,
|
||||||
fitPageToWindow: undefined,
|
fitPageToWindow: undefined,
|
||||||
scalePage: undefined,
|
scalePage: undefined,
|
||||||
readerType: undefined,
|
|
||||||
offsetFirstPage: undefined,
|
offsetFirstPage: undefined,
|
||||||
migration: undefined,
|
migration: undefined,
|
||||||
deleteChaptersManuallyMarkedRead: undefined,
|
deleteChaptersManuallyMarkedRead: undefined,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
|||||||
skipDupChapters: true,
|
skipDupChapters: true,
|
||||||
fitPageToWindow: true,
|
fitPageToWindow: true,
|
||||||
scalePage: false,
|
scalePage: false,
|
||||||
readerType: 'ContinuesVertical',
|
|
||||||
offsetFirstPage: false,
|
offsetFirstPage: false,
|
||||||
readerWidth: 50,
|
readerWidth: 50,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,20 +6,6 @@
|
|||||||
* 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 { GetChaptersReaderQuery, MangaReaderFieldsFragment } from '@/lib/graphql/generated/graphql.ts';
|
|
||||||
|
|
||||||
export type ReaderType =
|
|
||||||
| 'ContinuesVertical'
|
|
||||||
| 'Webtoon'
|
|
||||||
| 'SingleVertical'
|
|
||||||
| 'SingleRTL'
|
|
||||||
| 'SingleLTR'
|
|
||||||
| 'DoubleVertical'
|
|
||||||
| 'DoubleRTL'
|
|
||||||
| 'DoubleLTR'
|
|
||||||
| 'ContinuesHorizontalLTR'
|
|
||||||
| 'ContinuesHorizontalRTL';
|
|
||||||
|
|
||||||
export interface IReaderSettings {
|
export interface IReaderSettings {
|
||||||
staticNav: boolean;
|
staticNav: boolean;
|
||||||
showPageNumber: boolean;
|
showPageNumber: boolean;
|
||||||
@@ -27,25 +13,6 @@ export interface IReaderSettings {
|
|||||||
skipDupChapters: boolean;
|
skipDupChapters: boolean;
|
||||||
fitPageToWindow: boolean;
|
fitPageToWindow: boolean;
|
||||||
scalePage: boolean;
|
scalePage: boolean;
|
||||||
readerType: ReaderType;
|
|
||||||
offsetFirstPage: boolean;
|
offsetFirstPage: boolean;
|
||||||
readerWidth: number;
|
readerWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReaderPage {
|
|
||||||
index: number;
|
|
||||||
src: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IReaderProps {
|
|
||||||
pages: Array<IReaderPage>;
|
|
||||||
pageCount: number;
|
|
||||||
setCurPage: React.Dispatch<React.SetStateAction<number>>;
|
|
||||||
curPage: number;
|
|
||||||
initialPage: number;
|
|
||||||
settings: IReaderSettings;
|
|
||||||
manga: MangaReaderFieldsFragment;
|
|
||||||
chapter: GetChaptersReaderQuery['chapters']['nodes'][number];
|
|
||||||
nextChapter: () => void;
|
|
||||||
prevChapter: () => void;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,398 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 IconButton from '@mui/material/IconButton';
|
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
|
||||||
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
|
|
||||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
|
||||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
||||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
||||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import Slide from '@mui/material/Slide';
|
|
||||||
import Fade from '@mui/material/Fade';
|
|
||||||
import Zoom from '@mui/material/Zoom';
|
|
||||||
import Divider from '@mui/material/Divider';
|
|
||||||
import FormControl from '@mui/material/FormControl';
|
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
|
||||||
import { styled, useTheme } from '@mui/material/styles';
|
|
||||||
import ListItem from '@mui/material/ListItem';
|
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
|
||||||
import Collapse from '@mui/material/Collapse';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { ReaderSettingsOptions } from '@/modules/reader/components/ReaderSettingsOptions.tsx';
|
|
||||||
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
|
|
||||||
import { Select } from '@/modules/core/components/inputs/Select.tsx';
|
|
||||||
import { useGetOptionForDirection } from '@/theme.tsx';
|
|
||||||
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
|
|
||||||
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
|
||||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
|
||||||
import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx';
|
|
||||||
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
|
||||||
import { DirectionOffset } from '@/Base.types.ts';
|
|
||||||
import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts';
|
|
||||||
import { MangaChapterCountInfo, MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
|
||||||
|
|
||||||
const Root = styled('div')({
|
|
||||||
zIndex: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const NavContainer = styled('div')(({ theme }) => ({
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
minWidth: '240px',
|
|
||||||
maxWidth: '400px',
|
|
||||||
height: '100vh',
|
|
||||||
overflowY: 'auto',
|
|
||||||
backgroundColor: theme.palette.background.default,
|
|
||||||
|
|
||||||
'& header': {
|
|
||||||
backgroundColor: theme.palette.action.hover,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: `${theme.spacing(1)} ${theme.spacing(3)}`,
|
|
||||||
|
|
||||||
transition: 'left 2s ease',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Navigation = styled('div')(({ theme }) => ({
|
|
||||||
margin: `0 ${theme.spacing(2)}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const PageNavigation = styled('div')({
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
});
|
|
||||||
|
|
||||||
const ChapterNavigation = styled('div')(({ theme }) => ({
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateRows: 'auto auto auto',
|
|
||||||
gridTemplateColumns: '0fr 1fr 0fr',
|
|
||||||
gridTemplateAreas: '"pre current next"',
|
|
||||||
gridColumnGap: theme.spacing(0.5),
|
|
||||||
margin: `${theme.spacing(1)} 0`,
|
|
||||||
|
|
||||||
'& a': {
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: 'inherit',
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignContent: 'center',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
settings: IReaderSettings;
|
|
||||||
setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void;
|
|
||||||
manga: MangaIdInfo & MangaChapterCountInfo;
|
|
||||||
chapter: Pick<ChapterType, 'name' | 'sourceOrder' | 'pageCount'>;
|
|
||||||
chapters: Pick<ChapterType, 'id' | 'sourceOrder' | 'name' | 'chapterNumber' | 'scanlator'>[];
|
|
||||||
curPage: number;
|
|
||||||
scrollToPage: (page: number) => void;
|
|
||||||
openNextChapter: (offset: DirectionOffset) => void;
|
|
||||||
retrievingNextChapter: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ReaderNavBar(props: IProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { setReaderNavBarWidth } = useNavBarContext();
|
|
||||||
const theme = useTheme();
|
|
||||||
const getOptionForDirection = useGetOptionForDirection();
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation<{
|
|
||||||
prevDrawerOpen?: boolean;
|
|
||||||
prevSettingsCollapseOpen?: boolean;
|
|
||||||
}>();
|
|
||||||
const { prevDrawerOpen, prevSettingsCollapseOpen } = location.state ?? {};
|
|
||||||
|
|
||||||
const navBarRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
useResizeObserver(
|
|
||||||
navBarRef,
|
|
||||||
useCallback(() => {
|
|
||||||
if (navBarRef.current?.offsetWidth === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setReaderNavBarWidth(navBarRef.current.offsetWidth);
|
|
||||||
}, [navBarRef.current]),
|
|
||||||
);
|
|
||||||
useLayoutEffect(() => () => setReaderNavBarWidth(0), [navBarRef]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
settings,
|
|
||||||
setSettingValue,
|
|
||||||
manga,
|
|
||||||
chapter,
|
|
||||||
chapters,
|
|
||||||
curPage,
|
|
||||||
scrollToPage,
|
|
||||||
openNextChapter,
|
|
||||||
retrievingNextChapter,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const handleBack = useBackButton();
|
|
||||||
|
|
||||||
const hasMultipleScanlators = useMemo(
|
|
||||||
() => new Set(chapters.map(({ scanlator }) => scanlator)).size > 1,
|
|
||||||
[chapters],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [drawerOpen, setDrawerOpen] = useState(settings.staticNav || prevDrawerOpen);
|
|
||||||
const [updateDrawerOnRender, setUpdateDrawerOnRender] = useState(true);
|
|
||||||
const [hideOpenButton, setHideOpenButton] = useState(settings.staticNav || prevDrawerOpen);
|
|
||||||
const [prevScrollPos, setPrevScrollPos] = useState(0);
|
|
||||||
const [settingsCollapseOpen, setSettingsCollapseOpen] = useState(prevSettingsCollapseOpen ?? true);
|
|
||||||
|
|
||||||
const disableChapterNavButtons = retrievingNextChapter;
|
|
||||||
|
|
||||||
const updateSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => {
|
|
||||||
// prevent closing the navBar when updating the "staticNav" setting
|
|
||||||
setUpdateDrawerOnRender(key !== 'staticNav');
|
|
||||||
setSettingValue(key, value, persist);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDrawer = (open: boolean) => {
|
|
||||||
setDrawerOpen(open);
|
|
||||||
setHideOpenButton(open);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
const currentScrollPos = window.pageYOffset;
|
|
||||||
|
|
||||||
if (Math.abs(currentScrollPos - prevScrollPos) > 20) {
|
|
||||||
setHideOpenButton(currentScrollPos > prevScrollPos);
|
|
||||||
setPrevScrollPos(currentScrollPos);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (updateDrawerOnRender) {
|
|
||||||
updateDrawer(settings.staticNav);
|
|
||||||
}
|
|
||||||
}, [settings.staticNav]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
|
|
||||||
const rootEl: HTMLDivElement = document.querySelector('#root')!;
|
|
||||||
const mainContainer: HTMLDivElement = document.querySelector('#appMainContainer')!;
|
|
||||||
|
|
||||||
// main container and root div need to change styles...
|
|
||||||
rootEl.style.display = 'flex';
|
|
||||||
rootEl.style.flexDirection = 'column';
|
|
||||||
mainContainer.style.display = 'none';
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
rootEl.style.display = 'block';
|
|
||||||
mainContainer.style.display = 'block';
|
|
||||||
window.removeEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
}, [handleScroll]); // handleScroll changes on every render
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Root>
|
|
||||||
<Slide
|
|
||||||
direction={getOptionForDirection('right', 'left')}
|
|
||||||
in={drawerOpen}
|
|
||||||
timeout={200}
|
|
||||||
appear={false}
|
|
||||||
mountOnEnter
|
|
||||||
unmountOnExit
|
|
||||||
>
|
|
||||||
<NavContainer ref={navBarRef}>
|
|
||||||
<header>
|
|
||||||
{!settings.staticNav && (
|
|
||||||
<Tooltip title={t('reader.button.close_menu')}>
|
|
||||||
<IconButton
|
|
||||||
edge="start"
|
|
||||||
color="inherit"
|
|
||||||
aria-label="menu"
|
|
||||||
onClick={() => updateDrawer(false)}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
{getOptionForDirection(<KeyboardArrowLeftIcon />, <KeyboardArrowRightIcon />)}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
component="h1"
|
|
||||||
sx={{
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
overflow: 'hidden',
|
|
||||||
py: 1,
|
|
||||||
flexGrow: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{chapter.name}
|
|
||||||
</Typography>
|
|
||||||
<Tooltip title={t('reader.button.exit')}>
|
|
||||||
<IconButton
|
|
||||||
edge="start"
|
|
||||||
color="inherit"
|
|
||||||
aria-label="menu"
|
|
||||||
onClick={handleBack}
|
|
||||||
size="large"
|
|
||||||
sx={{ mr: -1 }}
|
|
||||||
>
|
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</header>
|
|
||||||
<ListItem
|
|
||||||
ContainerComponent="div"
|
|
||||||
sx={{
|
|
||||||
'& span': {
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemText primary={t('reader.settings.title.reader_settings')} />
|
|
||||||
<IconButton
|
|
||||||
edge="start"
|
|
||||||
color="inherit"
|
|
||||||
aria-label="menu"
|
|
||||||
disableRipple
|
|
||||||
disableFocusRipple
|
|
||||||
onClick={() => setSettingsCollapseOpen(!settingsCollapseOpen)}
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
{settingsCollapseOpen && <KeyboardArrowUpIcon />}
|
|
||||||
{!settingsCollapseOpen && <KeyboardArrowDownIcon />}
|
|
||||||
</IconButton>
|
|
||||||
</ListItem>
|
|
||||||
<Collapse in={settingsCollapseOpen} timeout="auto" unmountOnExit>
|
|
||||||
<ReaderSettingsOptions
|
|
||||||
setSettingValue={updateSettingValue}
|
|
||||||
staticNav={settings.staticNav}
|
|
||||||
showPageNumber={settings.showPageNumber}
|
|
||||||
loadNextOnEnding={settings.loadNextOnEnding}
|
|
||||||
skipDupChapters={settings.skipDupChapters}
|
|
||||||
fitPageToWindow={settings.fitPageToWindow}
|
|
||||||
scalePage={settings.scalePage}
|
|
||||||
readerType={settings.readerType}
|
|
||||||
offsetFirstPage={settings.offsetFirstPage}
|
|
||||||
readerWidth={settings.readerWidth}
|
|
||||||
/>
|
|
||||||
</Collapse>
|
|
||||||
<Divider sx={{ my: 1, mx: 2 }} />
|
|
||||||
<Navigation>
|
|
||||||
<PageNavigation>
|
|
||||||
<span>{t('reader.page_info.label.currently_on_page')}</span>
|
|
||||||
<FormControl
|
|
||||||
size="small"
|
|
||||||
sx={{ mx: 0.5 }}
|
|
||||||
disabled={disableChapterNavButtons || chapter.pageCount === -1}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={chapter.pageCount > -1 ? `${curPage}` : ''}
|
|
||||||
displayEmpty
|
|
||||||
onChange={({ target: { value: selectedPage } }) => {
|
|
||||||
scrollToPage(Number(selectedPage));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array(Math.max(0, chapter.pageCount))
|
|
||||||
.fill(1)
|
|
||||||
.map((ignoreValue, index) => (
|
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
<MenuItem key={`Page#${index}`} value={index}>
|
|
||||||
{index + 1}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<span>{t('reader.page_info.label.of_max_pages', { maxPages: chapter.pageCount })}</span>
|
|
||||||
</PageNavigation>
|
|
||||||
<ChapterNavigation>
|
|
||||||
<Tooltip title={t('reader.button.previous_chapter')}>
|
|
||||||
<IconButton
|
|
||||||
sx={{ gridArea: 'pre' }}
|
|
||||||
disabled={disableChapterNavButtons || chapter.sourceOrder <= 1}
|
|
||||||
onClick={() => openNextChapter(DirectionOffset.PREVIOUS)}
|
|
||||||
>
|
|
||||||
{getOptionForDirection(<KeyboardArrowLeftIcon />, <KeyboardArrowRightIcon />)}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<FormControl
|
|
||||||
sx={{ gridArea: 'current' }}
|
|
||||||
size="small"
|
|
||||||
disabled={disableChapterNavButtons || chapter.sourceOrder < 1}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={chapter.sourceOrder >= 1 ? `${chapter.sourceOrder}` : ''}
|
|
||||||
displayEmpty
|
|
||||||
onChange={({ target: { value: selectedChapter } }) => {
|
|
||||||
navigate(`/manga/${manga.id}/chapter/${selectedChapter}`, {
|
|
||||||
replace: true,
|
|
||||||
state: {
|
|
||||||
prevDrawerOpen: drawerOpen,
|
|
||||||
prevSettingsCollapseOpen: settingsCollapseOpen,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{chapters.map(({ id, sourceOrder, name, chapterNumber, scanlator }) => (
|
|
||||||
<MenuItem
|
|
||||||
key={id}
|
|
||||||
value={sourceOrder}
|
|
||||||
>{`#${chapterNumber}${hasMultipleScanlators && scanlator != null ? ` (${scanlator})` : ''} | ${name}`}</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<Tooltip title={t('reader.button.next_chapter')}>
|
|
||||||
<IconButton
|
|
||||||
sx={{ gridArea: 'next' }}
|
|
||||||
disabled={
|
|
||||||
disableChapterNavButtons ||
|
|
||||||
chapter.sourceOrder < 1 ||
|
|
||||||
chapter.sourceOrder >= manga.chapters.totalCount
|
|
||||||
}
|
|
||||||
onClick={() => openNextChapter(DirectionOffset.NEXT)}
|
|
||||||
>
|
|
||||||
{getOptionForDirection(<KeyboardArrowRightIcon />, <KeyboardArrowLeftIcon />)}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ChapterNavigation>
|
|
||||||
</Navigation>
|
|
||||||
</NavContainer>
|
|
||||||
</Slide>
|
|
||||||
<Zoom in={!drawerOpen}>
|
|
||||||
<Fade in={!hideOpenButton}>
|
|
||||||
<Tooltip title={t('reader.button.open_menu')}>
|
|
||||||
<CustomIconButton
|
|
||||||
sx={{
|
|
||||||
position: 'fixed',
|
|
||||||
top: 20,
|
|
||||||
left: 20,
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.75);',
|
|
||||||
color: 'black',
|
|
||||||
...theme.applyStyles('dark', {
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.75);',
|
|
||||||
color: 'white',
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
size="large"
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => updateDrawer(true)}
|
|
||||||
>
|
|
||||||
{getOptionForDirection(<KeyboardArrowRightIcon />, <KeyboardArrowLeftIcon />)}
|
|
||||||
</CustomIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Fade>
|
|
||||||
</Zoom>
|
|
||||||
</Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 List from '@mui/material/List';
|
|
||||||
import ListItem from '@mui/material/ListItem';
|
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
|
||||||
import Switch from '@mui/material/Switch';
|
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx';
|
|
||||||
import { isHorizontalReaderType } from '@/modules/reader/components/page/Page.tsx';
|
|
||||||
import { Select } from '@/modules/core/components/inputs/Select.tsx';
|
|
||||||
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
|
||||||
import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts';
|
|
||||||
|
|
||||||
interface IProps extends IReaderSettings {
|
|
||||||
setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ReaderSettingsOptions({
|
|
||||||
staticNav,
|
|
||||||
loadNextOnEnding,
|
|
||||||
readerType,
|
|
||||||
showPageNumber,
|
|
||||||
skipDupChapters,
|
|
||||||
setSettingValue,
|
|
||||||
fitPageToWindow,
|
|
||||||
scalePage,
|
|
||||||
offsetFirstPage,
|
|
||||||
readerWidth,
|
|
||||||
}: IProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const fitPageToWindowEligible = !isHorizontalReaderType(readerType);
|
|
||||||
return (
|
|
||||||
<List sx={{ pt: 0 }}>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary={t('reader.settings.label.static_navigation')} />
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={staticNav}
|
|
||||||
onChange={(e) => setSettingValue('staticNav', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary={t('reader.settings.label.show_page_number')} />
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={showPageNumber}
|
|
||||||
onChange={(e) => setSettingValue('showPageNumber', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary={t('reader.settings.label.load_next_chapter')} />
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={loadNextOnEnding}
|
|
||||||
onChange={(e) => setSettingValue('loadNextOnEnding', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary={t('reader.settings.label.skip_dup_chapters')} />
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={skipDupChapters}
|
|
||||||
onChange={(e) => setSettingValue('skipDupChapters', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
{fitPageToWindowEligible && (
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary={t('reader.settings.label.fit_page_to_window')} />
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={fitPageToWindow}
|
|
||||||
onChange={(e) => setSettingValue('fitPageToWindow', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
{fitPageToWindowEligible && fitPageToWindow && (
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary={t('reader.settings.label.scale_page')} />
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={scalePage}
|
|
||||||
onChange={(e) => setSettingValue('scalePage', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
{(readerType === 'DoubleLTR' || readerType === 'DoubleRTL') && (
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary={t('reader.settings.label.offset_first_page')} />
|
|
||||||
<Switch
|
|
||||||
edge="end"
|
|
||||||
checked={offsetFirstPage}
|
|
||||||
onChange={(e) => setSettingValue('offsetFirstPage', e.target.checked)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
{fitPageToWindowEligible && !fitPageToWindow && (
|
|
||||||
<NumberSetting
|
|
||||||
settingTitle={t('reader.settings.label.reader_width')}
|
|
||||||
settingValue={`${readerWidth}%`}
|
|
||||||
value={readerWidth}
|
|
||||||
minValue={10}
|
|
||||||
maxValue={100}
|
|
||||||
defaultValue={50}
|
|
||||||
valueUnit="%"
|
|
||||||
showSlider
|
|
||||||
handleUpdate={(width: number) => setSettingValue('readerWidth', width)}
|
|
||||||
handleLiveUpdate={(width: number) => setSettingValue('readerWidth', width, false)}
|
|
||||||
listItemTextSx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText primary={t('reader.settings.label.reader_type')} />
|
|
||||||
<Select
|
|
||||||
variant="standard"
|
|
||||||
value={readerType}
|
|
||||||
onChange={(e) => setSettingValue('readerType', e.target.value)}
|
|
||||||
sx={{ p: 0 }}
|
|
||||||
>
|
|
||||||
<MenuItem value="SingleLTR">{t('reader.settings.reader_type.label.single_page_ltr')}</MenuItem>
|
|
||||||
<MenuItem value="SingleRTL">{t('reader.settings.reader_type.label.single_page_rtl')}</MenuItem>
|
|
||||||
{/* <MenuItem value="SingleVertical">
|
|
||||||
Vertical(WIP)
|
|
||||||
</MenuItem> */}
|
|
||||||
<MenuItem value="DoubleLTR">{t('reader.settings.reader_type.label.double_page_ltr')}</MenuItem>
|
|
||||||
<MenuItem value="DoubleRTL">{t('reader.settings.reader_type.label.double_page_rtl')}</MenuItem>
|
|
||||||
<MenuItem value="Webtoon">{t('reader.settings.reader_type.label.webtoon')}</MenuItem>
|
|
||||||
<MenuItem value="ContinuesVertical">
|
|
||||||
{t('reader.settings.reader_type.label.continuous_vertical')}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="ContinuesHorizontalLTR">
|
|
||||||
{t('reader.settings.reader_type.label.continuous_horizontal_ltr')}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value="ContinuesHorizontalRTL">
|
|
||||||
{t('reader.settings.reader_type.label.continuous_horizontal_rtl')}
|
|
||||||
</MenuItem>
|
|
||||||
</Select>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { CSSProperties, forwardRef } from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
|
||||||
import { imageStyle } from '@/modules/reader/components/page/Page.tsx';
|
|
||||||
import { getOptionForDirection } from '@/theme.tsx';
|
|
||||||
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
index: number;
|
|
||||||
image1src: string;
|
|
||||||
image2src: string;
|
|
||||||
onImageLoad?: () => void;
|
|
||||||
settings: IReaderSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DoublePage = forwardRef((props: IProps, ref: any) => {
|
|
||||||
const { image1src, image2src, index, onImageLoad, settings } = props;
|
|
||||||
|
|
||||||
const baseImgStyle = imageStyle(settings);
|
|
||||||
const imgStyle = {
|
|
||||||
...baseImgStyle,
|
|
||||||
width: settings.fitPageToWindow ? baseImgStyle.width : `calc(${baseImgStyle.width} * 0.5)`,
|
|
||||||
minWidth:
|
|
||||||
settings.fitPageToWindow && settings.scalePage
|
|
||||||
? `calc(${baseImgStyle.minWidth} * 0.5)`
|
|
||||||
: baseImgStyle.minWidth,
|
|
||||||
maxWidth: settings.fitPageToWindow ? `calc(${baseImgStyle.maxWidth} * 0.5)` : baseImgStyle.maxWidth,
|
|
||||||
};
|
|
||||||
|
|
||||||
const spinnerStyle: CSSProperties = {
|
|
||||||
...imgStyle,
|
|
||||||
height: '100vh',
|
|
||||||
width: '50%',
|
|
||||||
backgroundColor: '#525252',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
ref={ref}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: settings.readerType === 'DoubleLTR' ? 'row' : 'row-reverse',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SpinnerImage
|
|
||||||
src={image1src}
|
|
||||||
onImageLoad={onImageLoad}
|
|
||||||
alt={`Page #${index}`}
|
|
||||||
spinnerStyle={spinnerStyle}
|
|
||||||
imgStyle={{
|
|
||||||
...imgStyle,
|
|
||||||
objectPosition:
|
|
||||||
settings.readerType === 'DoubleLTR'
|
|
||||||
? getOptionForDirection('right', 'left')
|
|
||||||
: getOptionForDirection('left', 'right'),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SpinnerImage
|
|
||||||
src={image2src}
|
|
||||||
onImageLoad={onImageLoad}
|
|
||||||
alt={`Page #${index + 1}`}
|
|
||||||
spinnerStyle={{
|
|
||||||
...spinnerStyle,
|
|
||||||
width: 'calc(50% - 5px)',
|
|
||||||
marginLeft: '5px',
|
|
||||||
}}
|
|
||||||
imgStyle={{
|
|
||||||
...imgStyle,
|
|
||||||
objectPosition:
|
|
||||||
settings.readerType === 'DoubleLTR'
|
|
||||||
? getOptionForDirection('left', 'right')
|
|
||||||
: getOptionForDirection('right', 'left'),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { CSSProperties, forwardRef } from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import { useTheme } from '@mui/material/styles';
|
|
||||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
|
||||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
|
||||||
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
|
||||||
import { IReaderSettings, ReaderType } from '@/modules/reader/Reader.types.ts';
|
|
||||||
import { Priority } from '@/lib/Queue.ts';
|
|
||||||
|
|
||||||
export const isHorizontalReaderType = (readerType: ReaderType): boolean =>
|
|
||||||
['ContinuesHorizontalLTR', 'ContinuesHorizontalRTL'].includes(readerType);
|
|
||||||
|
|
||||||
export function imageStyle(settings: IReaderSettings): CSSProperties {
|
|
||||||
const isDoublePageReader = ['DoubleRTL', 'DoubleLTR'].includes(settings.readerType);
|
|
||||||
const isVertical = settings.readerType === 'ContinuesVertical';
|
|
||||||
const isHorizontal = isHorizontalReaderType(settings.readerType);
|
|
||||||
|
|
||||||
const scrollbarHeight = MediaQuery.useGetScrollbarSize('height');
|
|
||||||
|
|
||||||
const baseStyling: CSSProperties = {
|
|
||||||
margin: 0,
|
|
||||||
width: `${settings.readerWidth}%`,
|
|
||||||
objectFit: 'contain',
|
|
||||||
};
|
|
||||||
|
|
||||||
const doublePageStyling: CSSProperties = {};
|
|
||||||
|
|
||||||
const continuesVerticalStyling: CSSProperties = {
|
|
||||||
marginBottom: '15px',
|
|
||||||
};
|
|
||||||
|
|
||||||
const continuesHorizontalStyling: CSSProperties = {
|
|
||||||
width: undefined,
|
|
||||||
minHeight: `calc(100vh - ${scrollbarHeight}px)`,
|
|
||||||
maxHeight: `calc(100vh - ${scrollbarHeight}px)`,
|
|
||||||
marginLeft: '7px',
|
|
||||||
marginRight: '7px',
|
|
||||||
};
|
|
||||||
|
|
||||||
const fitToPageStyling: CSSProperties = {
|
|
||||||
width: undefined,
|
|
||||||
height: undefined,
|
|
||||||
minWidth: settings.scalePage ? 'calc(100vw - (100vw - 100%))' : undefined,
|
|
||||||
maxWidth: 'calc(100vw - (100vw - 100%))',
|
|
||||||
minHeight: settings.scalePage ? `calc(100vh - ${scrollbarHeight}px)` : undefined,
|
|
||||||
maxHeight: `calc(100vh - ${scrollbarHeight}px)`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...baseStyling,
|
|
||||||
...(isDoublePageReader ? doublePageStyling : undefined),
|
|
||||||
...(isHorizontal ? continuesHorizontalStyling : undefined),
|
|
||||||
...(isVertical ? continuesVerticalStyling : undefined),
|
|
||||||
...(settings.fitPageToWindow && !isHorizontal ? fitToPageStyling : undefined),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
src: string;
|
|
||||||
index: number;
|
|
||||||
onImageLoad: () => void;
|
|
||||||
settings: IReaderSettings;
|
|
||||||
display?: boolean;
|
|
||||||
priority?: Priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Page = forwardRef((props: IProps, ref: any) => {
|
|
||||||
const { src, index, onImageLoad, settings, display = true, priority } = props;
|
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const isMobileWidth = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
|
|
||||||
const imgStyle = imageStyle(settings);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
ref={ref}
|
|
||||||
sx={{
|
|
||||||
display: display ? 'flex' : 'none',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SpinnerImage
|
|
||||||
priority={priority}
|
|
||||||
src={src}
|
|
||||||
onImageLoad={onImageLoad}
|
|
||||||
alt={`Page #${index}`}
|
|
||||||
spinnerStyle={{
|
|
||||||
...imgStyle,
|
|
||||||
height: '100vh',
|
|
||||||
// eslint-disable-next-line no-nested-ternary
|
|
||||||
width: isMobileWidth
|
|
||||||
? '100vw'
|
|
||||||
: isHorizontalReaderType(settings.readerType)
|
|
||||||
? '50vw'
|
|
||||||
: 'calc(100% * 0.5)',
|
|
||||||
backgroundColor: 'background.paper',
|
|
||||||
}}
|
|
||||||
imgStyle={imgStyle}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 Box from '@mui/material/Box';
|
|
||||||
|
|
||||||
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
settings: IReaderSettings;
|
|
||||||
curPage: number;
|
|
||||||
pageCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PageNumber(props: IProps) {
|
|
||||||
const { settings, curPage, pageCount } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: settings.showPageNumber ? 'block' : 'none',
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '50px',
|
|
||||||
padding: '2px',
|
|
||||||
paddingLeft: '4px',
|
|
||||||
paddingRight: '4px',
|
|
||||||
textAlign: 'center',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
|
||||||
borderRadius: '10px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{`${curPage + 1} / ${pageCount}`}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { MouseEvent, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import { Page } from '@/modules/reader/components/page/Page.tsx';
|
|
||||||
import { IReaderProps } from '@/modules/reader/Reader.types.ts';
|
|
||||||
import { Priority } from '@/lib/Queue.ts';
|
|
||||||
|
|
||||||
const isSpreadPage = (image: HTMLImageElement): boolean => {
|
|
||||||
const aspectRatio = image.height / image.width;
|
|
||||||
return aspectRatio < 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DoublePagedPager(props: IReaderProps) {
|
|
||||||
const { pages, settings, setCurPage, initialPage, curPage, nextChapter, prevChapter } = props;
|
|
||||||
|
|
||||||
const selfRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const pagesToDisplayStateRef = useRef<boolean[]>([]);
|
|
||||||
|
|
||||||
const [pagesToSpreadState, setPagesToSpreadState] = useState(Array(pages.length).fill(false));
|
|
||||||
const [pagesLoadState, setPagesLoadState] = useState<boolean[]>(Array(pages.length).fill(false));
|
|
||||||
|
|
||||||
// each spread page has to be counted as 2 pages and all trailing page numbers have to be increased by the count
|
|
||||||
// of leading spread pages
|
|
||||||
const pageToActualPageIndex = useMemo(
|
|
||||||
() => pagesToSpreadState.map((_, page) => page + pagesToSpreadState.slice(0, page).filter(Boolean).length),
|
|
||||||
[pagesToSpreadState],
|
|
||||||
);
|
|
||||||
|
|
||||||
function nextPage() {
|
|
||||||
const setNextPage = (page: number) => setCurPage(page === -1 ? pages.length - 1 : page);
|
|
||||||
|
|
||||||
const isLastPageDisplayed = pagesToDisplayStateRef.current[pages.length - 1];
|
|
||||||
if (isLastPageDisplayed && settings.loadNextOnEnding) {
|
|
||||||
// make sure to set last page as current page so that the chapter gets marked as read before opening the next chapter
|
|
||||||
const isLastPage = curPage === pages.length - 1;
|
|
||||||
if (!isLastPage) {
|
|
||||||
setNextPage(pages.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextChapter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = pagesToDisplayStateRef.current.findIndex((displayed, index) => !displayed && index > curPage);
|
|
||||||
setNextPage(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevPage() {
|
|
||||||
const setPrevPage = (page: number) => setCurPage(Math.max(page, 0));
|
|
||||||
|
|
||||||
const isFirstPageDisplayed = pagesToDisplayStateRef.current[0];
|
|
||||||
if (isFirstPageDisplayed) {
|
|
||||||
// not important, but for consistency make sure that first page gets set as current page in case it was displayed
|
|
||||||
const isFirstPage = !curPage;
|
|
||||||
if (!isFirstPage) {
|
|
||||||
setPrevPage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
prevChapter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = [...pagesToDisplayStateRef.current].slice(0, curPage).findLastIndex((displayed) => !displayed);
|
|
||||||
setPrevPage(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
function goLeft() {
|
|
||||||
if (settings.readerType === 'DoubleLTR') {
|
|
||||||
prevPage();
|
|
||||||
} else {
|
|
||||||
nextPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goRight() {
|
|
||||||
if (settings.readerType === 'DoubleLTR') {
|
|
||||||
nextPage();
|
|
||||||
} else {
|
|
||||||
prevPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyboardControl(e: KeyboardEvent) {
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Space':
|
|
||||||
e.preventDefault();
|
|
||||||
nextPage();
|
|
||||||
break;
|
|
||||||
case 'ArrowRight':
|
|
||||||
goRight();
|
|
||||||
break;
|
|
||||||
case 'ArrowLeft':
|
|
||||||
goLeft();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickControl(e: MouseEvent) {
|
|
||||||
if (e.clientX > window.innerWidth / 2) {
|
|
||||||
goRight();
|
|
||||||
} else {
|
|
||||||
goLeft();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('keydown', keyboardControl);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', keyboardControl);
|
|
||||||
};
|
|
||||||
}, [selfRef, curPage, settings.readerType, prevChapter, nextChapter, pagesLoadState, pagesToSpreadState]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCurPage(initialPage);
|
|
||||||
}, [initialPage]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box ref={selfRef} onClick={clickControl}>
|
|
||||||
<Box
|
|
||||||
id="display"
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: settings.readerType === 'DoubleLTR' ? 'row' : 'row-reverse',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: 'auto',
|
|
||||||
height: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{pages.map(({ index, src }) => {
|
|
||||||
/*
|
|
||||||
|
|
||||||
| = page separator
|
|
||||||
+ = double page
|
|
||||||
_ = double spread
|
|
||||||
|
|
||||||
without double spreads:
|
|
||||||
without double spread offset: | 0 + 1 | 2 + 3 | 4 + 5 | 6 + 7 | 8 |
|
|
||||||
with double spread offset : | 0 | 1 + 2 | 3 + 4 | 5 + 6 | 7 + 8 |
|
|
||||||
|
|
||||||
with double spreads
|
|
||||||
to handle double spreads:
|
|
||||||
each double spread has to count as 2 pages, thus, each page number after a double spread has to increase
|
|
||||||
by the number of leading double spreads
|
|
||||||
|
|
||||||
without double spread offset: | 0 + 1 | 2 | _3/4_ | 5 + 6 | 7 + 8 | _9/10_ | 11 + 12 | 13 + 14 |
|
|
||||||
with double spread offset : | 0 | 1 + 2 | _3/4_ | 5 + 6 | 7 + 8 | _9/10_ | 11 + 12 | 13 + 14 |
|
|
||||||
|
|
||||||
thus, to get the second page:
|
|
||||||
without offset: second page = current page number even ? +1 : -1
|
|
||||||
with offset : second page = current page number even ? +1 : -1
|
|
||||||
|
|
||||||
the second page has to be ignored in case:
|
|
||||||
- double spreads are offset, and the current page is the first page
|
|
||||||
- either the current or second page is a double spread
|
|
||||||
*/
|
|
||||||
|
|
||||||
const normalizedCurPage = pageToActualPageIndex[curPage];
|
|
||||||
const isCurPageEven = !(normalizedCurPage % 2);
|
|
||||||
|
|
||||||
const secondPageOffset = (() => {
|
|
||||||
const invert = settings.offsetFirstPage ? -1 : 1;
|
|
||||||
const offset = isCurPageEven ? 1 : -1;
|
|
||||||
|
|
||||||
return offset * invert;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const secondPage = curPage + secondPageOffset;
|
|
||||||
|
|
||||||
const isFirstPage = curPage === 0;
|
|
||||||
const isCurPage = index === curPage;
|
|
||||||
const isSecondPage = index === secondPage;
|
|
||||||
|
|
||||||
const isCurrentPageSpreadPage = pagesToSpreadState[curPage];
|
|
||||||
const isSecondPageSpreadPage = pagesToSpreadState[secondPage];
|
|
||||||
const hasSpreadPage = isCurrentPageSpreadPage || isSecondPageSpreadPage;
|
|
||||||
|
|
||||||
const ignoreSecondPageDueToOffset = isFirstPage && settings.offsetFirstPage;
|
|
||||||
|
|
||||||
const displaySecondPage = isSecondPage && !hasSpreadPage && !ignoreSecondPageDueToOffset;
|
|
||||||
const displayPage = isCurPage || displaySecondPage;
|
|
||||||
|
|
||||||
pagesToDisplayStateRef.current[index] = displayPage;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
key={src}
|
|
||||||
index={index}
|
|
||||||
src={src}
|
|
||||||
onImageLoad={() => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
setPagesLoadState((prevState) => prevState.toSpliced(index, 1, true));
|
|
||||||
setPagesToSpreadState((prevState) =>
|
|
||||||
prevState.toSpliced(index, 1, isSpreadPage(img)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
img.src = src;
|
|
||||||
}}
|
|
||||||
settings={settings}
|
|
||||||
display={displayPage}
|
|
||||||
priority={displayPage ? Priority.HIGH : undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { MouseEvent as ReactMouseEvent, WheelEvent as ReactWheelEvent, useCallback, useEffect, useRef } from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import { Page } from '@/modules/reader/components/page/Page.tsx';
|
|
||||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
|
||||||
import { IReaderProps } from '@/modules/reader/Reader.types.ts';
|
|
||||||
import { Priority } from '@/lib/Queue.ts';
|
|
||||||
|
|
||||||
const findCurrentPageIndex = (wrapper: HTMLDivElement): number => {
|
|
||||||
for (let i = 0; i < wrapper.children.length; i++) {
|
|
||||||
const child = wrapper.children.item(i);
|
|
||||||
if (child) {
|
|
||||||
const { left, right } = child.getBoundingClientRect();
|
|
||||||
if (left <= window.innerWidth / 2 && right > window.innerWidth / 2) return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SCROLL_SAFE_ZONE = 5; // px
|
|
||||||
const isAtEnd = () => {
|
|
||||||
const visibleEnd = window.innerWidth + window.scrollX;
|
|
||||||
// SCROLL_SAFE_ZONE is here for special cases when window might be .5px shorter
|
|
||||||
// and math just dont add up correctly
|
|
||||||
return visibleEnd >= document.body.scrollWidth - SCROLL_SAFE_ZONE;
|
|
||||||
};
|
|
||||||
const isAtStart = () => window.scrollX <= 0;
|
|
||||||
|
|
||||||
export function HorizontalPager(props: IReaderProps) {
|
|
||||||
const { pages, curPage, initialPage, settings, setCurPage, prevChapter, nextChapter } = props;
|
|
||||||
|
|
||||||
const currentPageRef = useRef(initialPage);
|
|
||||||
const selfRef = useRef<HTMLDivElement>(null);
|
|
||||||
const pagesRef = useRef<HTMLDivElement[]>([]);
|
|
||||||
|
|
||||||
function nextPage() {
|
|
||||||
if (curPage < pages.length - 1) {
|
|
||||||
pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' });
|
|
||||||
setCurPage((page) => page + 1);
|
|
||||||
} else if (settings.loadNextOnEnding) {
|
|
||||||
nextChapter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevPage() {
|
|
||||||
if (curPage > 0) {
|
|
||||||
pagesRef.current[curPage - 1]?.scrollIntoView({ inline: 'center' });
|
|
||||||
setCurPage(curPage - 1);
|
|
||||||
} else if (curPage === 0) {
|
|
||||||
prevChapter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goLeft() {
|
|
||||||
if (settings.readerType === 'ContinuesHorizontalLTR') {
|
|
||||||
prevPage();
|
|
||||||
} else {
|
|
||||||
nextPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goRight() {
|
|
||||||
if (settings.readerType === 'ContinuesHorizontalLTR') {
|
|
||||||
nextPage();
|
|
||||||
} else {
|
|
||||||
prevPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mouseXPos = useRef<number>(0);
|
|
||||||
|
|
||||||
function dragScreen(e: MouseEvent) {
|
|
||||||
window.scrollBy(mouseXPos.current - e.pageX, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragControl(e: MouseEvent) {
|
|
||||||
mouseXPos.current = e.pageX;
|
|
||||||
selfRef.current?.addEventListener('mousemove', dragScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeDragControl() {
|
|
||||||
selfRef.current?.removeEventListener('mousemove', dragScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickControl(e: ReactMouseEvent) {
|
|
||||||
if (e.clientX >= window.innerWidth * 0.85) {
|
|
||||||
goRight();
|
|
||||||
} else if (e.clientX <= window.innerWidth * 0.15) {
|
|
||||||
goLeft();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function horizontalScroll(e: ReactWheelEvent) {
|
|
||||||
window.scrollBy({
|
|
||||||
left: settings.readerType === 'ContinuesHorizontalLTR' ? e.deltaY : e.deltaY * -1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLoadNextonEnding = () => {
|
|
||||||
if (settings.readerType === 'ContinuesHorizontalLTR') {
|
|
||||||
if (window.scrollX + window.innerWidth >= document.body.scrollWidth) {
|
|
||||||
nextChapter();
|
|
||||||
}
|
|
||||||
} else if (settings.readerType === 'ContinuesHorizontalRTL') {
|
|
||||||
if (window.scrollX <= window.innerWidth) {
|
|
||||||
nextChapter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useResizeObserver(
|
|
||||||
pagesRef.current[initialPage],
|
|
||||||
useCallback(
|
|
||||||
(_, resizeObserver) => {
|
|
||||||
const initialPageElement = pagesRef.current[initialPage];
|
|
||||||
if (!initialPageElement?.offsetHeight) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialPageElement.scrollIntoView({ inline: 'center' });
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
},
|
|
||||||
[pagesRef.current[initialPage], initialPage],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
selfRef.current?.addEventListener('mousedown', dragControl);
|
|
||||||
selfRef.current?.addEventListener('mouseup', removeDragControl);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
selfRef.current?.removeEventListener('mousedown', dragControl);
|
|
||||||
selfRef.current?.removeEventListener('mouseup', removeDragControl);
|
|
||||||
};
|
|
||||||
}, [selfRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (settings.loadNextOnEnding) {
|
|
||||||
document.addEventListener('scroll', handleLoadNextonEnding);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('scroll', handleLoadNextonEnding);
|
|
||||||
};
|
|
||||||
}, [selfRef, curPage, prevChapter, nextChapter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!selfRef.current) return;
|
|
||||||
|
|
||||||
// Update current page in parent
|
|
||||||
const currentPage = findCurrentPageIndex(selfRef.current);
|
|
||||||
if (currentPage !== currentPageRef.current) {
|
|
||||||
currentPageRef.current = currentPage;
|
|
||||||
setCurPage(currentPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special case if scroll is moved all the way to the edge
|
|
||||||
// This handles cases when last page is show, but is smaller then
|
|
||||||
// window, in which case it would never get marked as read.
|
|
||||||
// See https://github.com/Suwayomi/Suwayomi-WebUI/issues/14 for more info
|
|
||||||
if (settings.readerType === 'ContinuesHorizontalLTR' ? isAtEnd() : isAtStart()) {
|
|
||||||
currentPageRef.current = pages.length - 1;
|
|
||||||
setCurPage(currentPageRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
|
||||||
}, [settings.readerType]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
ref={selfRef}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: settings.readerType === 'ContinuesHorizontalLTR' ? 'row' : 'row-reverse',
|
|
||||||
justifyContent: settings.readerType === 'ContinuesHorizontalLTR' ? 'flex-start' : 'flex-end',
|
|
||||||
width: 'auto',
|
|
||||||
height: 'auto',
|
|
||||||
overflowX: 'visible',
|
|
||||||
userSelect: 'none',
|
|
||||||
}}
|
|
||||||
onClick={clickControl}
|
|
||||||
onWheel={horizontalScroll}
|
|
||||||
>
|
|
||||||
{pages.map((page) => (
|
|
||||||
<Page
|
|
||||||
key={page.index}
|
|
||||||
index={page.index}
|
|
||||||
src={page.src}
|
|
||||||
onImageLoad={() => {}}
|
|
||||||
settings={settings}
|
|
||||||
ref={(e: HTMLDivElement) => {
|
|
||||||
pagesRef.current[page.index] = e;
|
|
||||||
}}
|
|
||||||
priority={page.index === curPage ? Priority.HIGH : undefined}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { MouseEvent, useEffect, useRef } from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import { Page } from '@/modules/reader/components/page/Page.tsx';
|
|
||||||
import { IReaderProps } from '@/modules/reader/Reader.types.ts';
|
|
||||||
import { Priority } from '@/lib/Queue.ts';
|
|
||||||
|
|
||||||
export function PagedPager(props: IReaderProps) {
|
|
||||||
const { pages, settings, setCurPage, initialPage, curPage, nextChapter, prevChapter } = props;
|
|
||||||
|
|
||||||
const selfRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const changePage = (newPage: number) => {
|
|
||||||
setCurPage(newPage);
|
|
||||||
window.scroll({ top: 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
function nextPage() {
|
|
||||||
if (curPage < pages.length - 1) {
|
|
||||||
changePage(curPage + 1);
|
|
||||||
} else if (settings.loadNextOnEnding) {
|
|
||||||
nextChapter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function prevPage() {
|
|
||||||
if (curPage > 0) {
|
|
||||||
changePage(curPage - 1);
|
|
||||||
} else {
|
|
||||||
prevChapter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goLeft() {
|
|
||||||
if (settings.readerType === 'SingleLTR') {
|
|
||||||
prevPage();
|
|
||||||
} else if (settings.readerType === 'SingleRTL') {
|
|
||||||
nextPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function goRight() {
|
|
||||||
if (settings.readerType === 'SingleLTR') {
|
|
||||||
nextPage();
|
|
||||||
} else if (settings.readerType === 'SingleRTL') {
|
|
||||||
prevPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function keyboardControl(e: KeyboardEvent) {
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Space':
|
|
||||||
e.preventDefault();
|
|
||||||
nextPage();
|
|
||||||
break;
|
|
||||||
case 'ArrowRight':
|
|
||||||
goRight();
|
|
||||||
break;
|
|
||||||
case 'ArrowLeft':
|
|
||||||
goLeft();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickControl(e: MouseEvent) {
|
|
||||||
if (e.clientX > window.innerWidth / 2) {
|
|
||||||
goRight();
|
|
||||||
} else {
|
|
||||||
goLeft();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('keydown', keyboardControl);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', keyboardControl);
|
|
||||||
};
|
|
||||||
}, [selfRef, curPage, settings.readerType, prevChapter, nextChapter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Delay scrolling to next cycle
|
|
||||||
setTimeout(() => {
|
|
||||||
// scroll last read page into view when initialPage changes
|
|
||||||
changePage(initialPage);
|
|
||||||
}, 0);
|
|
||||||
}, [initialPage]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
ref={selfRef}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: 'auto',
|
|
||||||
height: 'auto',
|
|
||||||
}}
|
|
||||||
onClick={clickControl}
|
|
||||||
>
|
|
||||||
{pages.map(({ index, src }) => (
|
|
||||||
<Page
|
|
||||||
key={src}
|
|
||||||
index={index}
|
|
||||||
onImageLoad={() => {}}
|
|
||||||
src={src}
|
|
||||||
settings={settings}
|
|
||||||
display={index === curPage}
|
|
||||||
priority={index === curPage ? Priority.HIGH : undefined}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { useCallback, useEffect, useRef } from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import { Page } from '@/modules/reader/components/page/Page.tsx';
|
|
||||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
|
||||||
import { IReaderProps } from '@/modules/reader/Reader.types.ts';
|
|
||||||
import { Priority } from '@/lib/Queue.ts';
|
|
||||||
|
|
||||||
const findCurrentPageIndex = (wrapper: HTMLDivElement): number => {
|
|
||||||
for (let i = 0; i < wrapper.children.length; i++) {
|
|
||||||
const child = wrapper.children.item(i);
|
|
||||||
if (child) {
|
|
||||||
const { top, bottom } = child.getBoundingClientRect();
|
|
||||||
if (top <= window.innerHeight && bottom > 1) return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: make configurable?
|
|
||||||
const SCROLL_SAFE_ZONE = 5; // px
|
|
||||||
const SCROLL_OFFSET = 0.95;
|
|
||||||
const SCROLL_OFFSET_SLIGHT = 0.25;
|
|
||||||
const SCROLL_BEHAVIOR: ScrollBehavior = 'smooth';
|
|
||||||
|
|
||||||
const isAtBottom = () => {
|
|
||||||
const visibleBottom = window.innerHeight + window.scrollY;
|
|
||||||
// SCROLL_SAFE_ZONE is here for special cases when window might be .5px shorter
|
|
||||||
// and math just dont add up correctly
|
|
||||||
return visibleBottom >= document.body.offsetHeight - SCROLL_SAFE_ZONE;
|
|
||||||
};
|
|
||||||
const isAtTop = () => window.scrollY <= 0;
|
|
||||||
|
|
||||||
export function VerticalPager(props: IReaderProps) {
|
|
||||||
const { curPage, pages, settings, setCurPage, initialPage, nextChapter, prevChapter } = props;
|
|
||||||
|
|
||||||
const currentPageRef = useRef(initialPage);
|
|
||||||
const selfRef = useRef<HTMLDivElement>(null);
|
|
||||||
const pagesRef = useRef<HTMLDivElement[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let handlingEndOfPage = false;
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!selfRef.current) return;
|
|
||||||
|
|
||||||
if (isAtBottom()) {
|
|
||||||
if (handlingEndOfPage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlingEndOfPage = true;
|
|
||||||
|
|
||||||
// If scroll is moved all the way to the bottom
|
|
||||||
// This handles cases when last page is show, but is smaller then
|
|
||||||
// window, in which case it would never get marked as read.
|
|
||||||
// See https://github.com/Suwayomi/Suwayomi-WebUI/issues/14 for more info
|
|
||||||
currentPageRef.current = pages.length - 1;
|
|
||||||
setCurPage(currentPageRef.current);
|
|
||||||
|
|
||||||
// Go to next chapter if configured to and at bottom
|
|
||||||
if (settings.loadNextOnEnding) {
|
|
||||||
nextChapter();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handlingEndOfPage = false;
|
|
||||||
|
|
||||||
// Update current page in parent
|
|
||||||
const currentPage = findCurrentPageIndex(selfRef.current);
|
|
||||||
if (currentPage !== currentPageRef.current) {
|
|
||||||
currentPageRef.current = currentPage;
|
|
||||||
setCurPage(currentPage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('scroll', handleScroll);
|
|
||||||
};
|
|
||||||
}, [settings.loadNextOnEnding, nextChapter]);
|
|
||||||
|
|
||||||
const mouseYPos = useRef<number>(0);
|
|
||||||
const didMouseMove = useRef(false);
|
|
||||||
|
|
||||||
function dragScreen(e: MouseEvent) {
|
|
||||||
didMouseMove.current = true;
|
|
||||||
window.scrollBy(0, mouseYPos.current - e.pageY);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragControl(e: MouseEvent) {
|
|
||||||
mouseYPos.current = e.pageY;
|
|
||||||
selfRef.current?.addEventListener('mousemove', dragScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeDragControl() {
|
|
||||||
selfRef.current?.removeEventListener('mousemove', dragScreen);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
selfRef.current?.addEventListener('mousedown', dragControl);
|
|
||||||
selfRef.current?.addEventListener('mouseup', removeDragControl);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
selfRef.current?.removeEventListener('mousedown', dragControl);
|
|
||||||
selfRef.current?.removeEventListener('mouseup', removeDragControl);
|
|
||||||
};
|
|
||||||
}, [selfRef]);
|
|
||||||
|
|
||||||
const go = useCallback(
|
|
||||||
(direction: 'up' | 'down', offset: number = SCROLL_OFFSET) => {
|
|
||||||
if (direction === 'down' && isAtBottom()) {
|
|
||||||
nextChapter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (direction === 'up' && isAtTop()) {
|
|
||||||
prevChapter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.scroll({
|
|
||||||
top: window.scrollY + window.innerHeight * offset * (direction === 'up' ? -1 : 1),
|
|
||||||
behavior: SCROLL_BEHAVIOR,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[nextChapter, prevChapter],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyboard = (e: KeyboardEvent) => {
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Space':
|
|
||||||
e.preventDefault();
|
|
||||||
go(e.shiftKey ? 'up' : 'down');
|
|
||||||
break;
|
|
||||||
case 'ArrowDown':
|
|
||||||
e.preventDefault();
|
|
||||||
go(e.shiftKey ? 'up' : 'down', SCROLL_OFFSET_SLIGHT);
|
|
||||||
break;
|
|
||||||
case 'ArrowRight':
|
|
||||||
e.preventDefault();
|
|
||||||
go(e.shiftKey ? 'up' : 'down');
|
|
||||||
break;
|
|
||||||
case 'ArrowUp':
|
|
||||||
e.preventDefault();
|
|
||||||
go(e.shiftKey ? 'down' : 'up', SCROLL_OFFSET_SLIGHT);
|
|
||||||
break;
|
|
||||||
case 'ArrowLeft':
|
|
||||||
e.preventDefault();
|
|
||||||
go(e.shiftKey ? 'down' : 'up');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyboard, false);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('keydown', handleKeyboard);
|
|
||||||
};
|
|
||||||
}, [go]);
|
|
||||||
|
|
||||||
useResizeObserver(
|
|
||||||
pagesRef.current[initialPage],
|
|
||||||
useCallback(
|
|
||||||
(_, resizeObserver) => {
|
|
||||||
const initialPageElement = pagesRef.current[initialPage];
|
|
||||||
if (!initialPageElement?.offsetHeight) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialPageElement.scrollIntoView();
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
},
|
|
||||||
[pagesRef.current[initialPage], initialPage],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
ref={selfRef}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: 'auto',
|
|
||||||
height: 'auto',
|
|
||||||
userSelect: 'none',
|
|
||||||
}}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (didMouseMove.current) {
|
|
||||||
didMouseMove.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
go(e.clientX > window.innerWidth / 2 ? 'down' : 'up');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{pages.map((page) => (
|
|
||||||
<Page
|
|
||||||
key={page.index}
|
|
||||||
index={page.index}
|
|
||||||
src={page.src}
|
|
||||||
onImageLoad={() => {}}
|
|
||||||
settings={settings}
|
|
||||||
ref={(e: HTMLDivElement) => {
|
|
||||||
pagesRef.current[page.index] = e;
|
|
||||||
}}
|
|
||||||
priority={page.index === curPage ? Priority.HIGH : undefined}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 { useContext, useLayoutEffect } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { requestUpdateServerMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
|
||||||
import { useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
|
||||||
import { ReaderSettingsOptions } from '@/modules/reader/components/ReaderSettingsOptions.tsx';
|
|
||||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
|
||||||
import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
|
||||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
|
||||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
|
||||||
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
|
||||||
import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts';
|
|
||||||
|
|
||||||
export function DefaultReaderSettings() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { setTitle, setAction } = useContext(NavBarContext);
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
setTitle(t('reader.settings.title.default_reader_settings'));
|
|
||||||
setAction(null);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setTitle('');
|
|
||||||
setAction(null);
|
|
||||||
};
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
settings,
|
|
||||||
loading,
|
|
||||||
request: { error, refetch },
|
|
||||||
} = useDefaultReaderSettings();
|
|
||||||
|
|
||||||
const setSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist: boolean = true) => {
|
|
||||||
if (persist) {
|
|
||||||
requestUpdateServerMetadata([[key, value]]).catch(() =>
|
|
||||||
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <LoadingPlaceholder />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<EmptyViewAbsoluteCentered
|
|
||||||
message={t('global.error.label.failed_to_load_data')}
|
|
||||||
messageExtra={error.message}
|
|
||||||
retry={() => refetch().catch(defaultPromiseErrorHandler('DefaultReaderSettings::refetch'))}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReaderSettingsOptions
|
|
||||||
setSettingValue={setSettingValue}
|
|
||||||
staticNav={settings.staticNav}
|
|
||||||
showPageNumber={settings.showPageNumber}
|
|
||||||
loadNextOnEnding={settings.loadNextOnEnding}
|
|
||||||
skipDupChapters={settings.skipDupChapters}
|
|
||||||
fitPageToWindow={settings.fitPageToWindow}
|
|
||||||
scalePage={settings.scalePage}
|
|
||||||
readerType={settings.readerType}
|
|
||||||
offsetFirstPage={settings.offsetFirstPage}
|
|
||||||
readerWidth={settings.readerWidth}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,576 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 CircularProgress from '@mui/material/CircularProgress';
|
|
||||||
import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
|
||||||
import { getReaderSettingsFor, useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
|
||||||
import { requestUpdateMangaMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
|
||||||
import { HorizontalPager } from '@/modules/reader/components/pager/HorizontalPager.tsx';
|
|
||||||
import { PageNumber } from '@/modules/reader/components/page/PageNumber.tsx';
|
|
||||||
import { PagedPager } from '@/modules/reader/components/pager/PagedPager.tsx';
|
|
||||||
import { DoublePagedPager } from '@/modules/reader/components/pager/DoublePagedPager.tsx';
|
|
||||||
import { VerticalPager } from '@/modules/reader/components/pager/VerticalPager.tsx';
|
|
||||||
import { ReaderNavBar } from '@/modules/reader/components/ReaderNavBar.tsx';
|
|
||||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
|
||||||
import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
|
||||||
import { useDebounce } from '@/modules/core/hooks/useDebounce.ts';
|
|
||||||
import {
|
|
||||||
GetChaptersReaderQuery,
|
|
||||||
GetChaptersReaderQueryVariables,
|
|
||||||
GetMangaReaderQuery,
|
|
||||||
UpdateChapterPatchInput,
|
|
||||||
} from '@/lib/graphql/generated/graphql.ts';
|
|
||||||
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
|
||||||
import { ChapterIdInfo, Chapters } from '@/modules/chapter/services/Chapters.ts';
|
|
||||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
|
||||||
import { GET_CHAPTERS_READER } from '@/lib/graphql/queries/ChapterQuery.ts';
|
|
||||||
import { GET_MANGA_READER } from '@/lib/graphql/queries/MangaQuery.ts';
|
|
||||||
import { CHAPTER_READER_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts';
|
|
||||||
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
|
||||||
import { IReaderSettings, ReaderType } from '@/modules/reader/Reader.types.ts';
|
|
||||||
import { DirectionOffset } from '@/Base.types.ts';
|
|
||||||
import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts';
|
|
||||||
import { TMangaReader } from '@/modules/manga/Manga.types.ts';
|
|
||||||
|
|
||||||
type TChapter = GetChaptersReaderQuery['chapters']['nodes'][number];
|
|
||||||
|
|
||||||
const getChapterFromCache = (id: ChapterIdInfo['id']): TChapter | null =>
|
|
||||||
Chapters.getFromCache<TChapter>(id, CHAPTER_READER_FIELDS, 'CHAPTER_READER_FIELDS')!;
|
|
||||||
|
|
||||||
const getReaderComponent = (readerType: ReaderType) => {
|
|
||||||
switch (readerType) {
|
|
||||||
case 'ContinuesVertical':
|
|
||||||
case 'Webtoon':
|
|
||||||
return VerticalPager;
|
|
||||||
break;
|
|
||||||
case 'SingleVertical':
|
|
||||||
case 'SingleRTL':
|
|
||||||
case 'SingleLTR':
|
|
||||||
return PagedPager;
|
|
||||||
break;
|
|
||||||
case 'DoubleVertical':
|
|
||||||
case 'DoubleRTL':
|
|
||||||
case 'DoubleLTR':
|
|
||||||
return DoublePagedPager;
|
|
||||||
break;
|
|
||||||
case 'ContinuesHorizontalLTR':
|
|
||||||
case 'ContinuesHorizontalRTL':
|
|
||||||
return HorizontalPager;
|
|
||||||
default:
|
|
||||||
return VerticalPager;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const range = (n: number) => Array.from({ length: n }, (value, key) => key);
|
|
||||||
const fallbackChapter = {
|
|
||||||
pageCount: -1,
|
|
||||||
sourceOrder: -1,
|
|
||||||
chapterCount: 0,
|
|
||||||
lastPageRead: 0,
|
|
||||||
name: 'Loading...',
|
|
||||||
manga: { id: -1 },
|
|
||||||
} as unknown as TChapter;
|
|
||||||
|
|
||||||
export function Reader() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>();
|
|
||||||
|
|
||||||
const fallbackManga = useMemo(
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
id: +mangaId,
|
|
||||||
title: '',
|
|
||||||
thumbnailUrl: '',
|
|
||||||
genre: [],
|
|
||||||
inLibraryAt: 0,
|
|
||||||
lastReadAt: 0,
|
|
||||||
chapters: { totalCount: 0 },
|
|
||||||
trackRecords: { totalCount: 0 },
|
|
||||||
}) as unknown as TMangaReader,
|
|
||||||
[mangaId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
loading: isMangaLoading,
|
|
||||||
error: mangaError,
|
|
||||||
refetch: refetchManga,
|
|
||||||
} = requestManager.useGetManga<GetMangaReaderQuery>(GET_MANGA_READER, mangaId);
|
|
||||||
const loadedChapter = useRef<TChapter | null>(null);
|
|
||||||
const isChapterLoaded =
|
|
||||||
Number(mangaId) === loadedChapter.current?.mangaId &&
|
|
||||||
Number(chapterIndex) === loadedChapter.current?.sourceOrder &&
|
|
||||||
loadedChapter.current?.pageCount !== -1;
|
|
||||||
const manga = data?.manga ?? fallbackManga;
|
|
||||||
const {
|
|
||||||
data: chaptersData,
|
|
||||||
loading: isChapterLoading,
|
|
||||||
error: chapterError,
|
|
||||||
refetch: fetchChapter,
|
|
||||||
} = requestManager.useGetChapters<GetChaptersReaderQuery, GetChaptersReaderQueryVariables>(
|
|
||||||
GET_CHAPTERS_READER,
|
|
||||||
{
|
|
||||||
condition: { mangaId: Number(mangaId), sourceOrder: Number(chapterIndex) },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
notifyOnNetworkStatusChange: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const chapterData = chaptersData?.chapters.nodes[0];
|
|
||||||
const arePagesUpdatedRef = useRef(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
settings: { downloadAheadLimit },
|
|
||||||
} = useMetadataServerSettings();
|
|
||||||
const isDownloadAheadEnabled = !!downloadAheadLimit;
|
|
||||||
|
|
||||||
const getLoadedChapter = () => {
|
|
||||||
const isAChapterLoaded = loadedChapter.current;
|
|
||||||
|
|
||||||
const isSameAsLoadedChapter = isAChapterLoaded && isChapterLoaded;
|
|
||||||
if (isSameAsLoadedChapter) {
|
|
||||||
const didPageCountChange = chapterData && loadedChapter.current?.pageCount !== chapterData.pageCount;
|
|
||||||
return didPageCountChange ? chapterData! : loadedChapter.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
arePagesUpdatedRef.current = false;
|
|
||||||
|
|
||||||
if (chapterData) {
|
|
||||||
return chapterData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
loadedChapter.current = getLoadedChapter();
|
|
||||||
|
|
||||||
const chapter = loadedChapter.current ?? fallbackChapter;
|
|
||||||
|
|
||||||
const initialChapterRef = useRef<TChapter>(fallbackChapter);
|
|
||||||
if (initialChapterRef.current === fallbackChapter) {
|
|
||||||
initialChapterRef.current = chapter;
|
|
||||||
}
|
|
||||||
if (chapter.mangaId !== initialChapterRef.current?.mangaId) {
|
|
||||||
initialChapterRef.current = fallbackChapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [fetchPages, { loading: arePagesLoading, error: pagesError }] = requestManager.useGetChapterPagesFetch(
|
|
||||||
chapter.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const doFetchPages = () => {
|
|
||||||
if (isChapterLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchPages()
|
|
||||||
.then(() => {
|
|
||||||
arePagesUpdatedRef.current = true;
|
|
||||||
})
|
|
||||||
.catch(defaultPromiseErrorHandler('Reader::fetchPages'));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
doFetchPages();
|
|
||||||
}, [chapter.id]);
|
|
||||||
|
|
||||||
const [wasLastPageReadSet, setWasLastPageReadSet] = useState(false);
|
|
||||||
const [curPage, setCurPage] = useState<number>(0);
|
|
||||||
const isLastPage = curPage === chapter.pageCount - 1;
|
|
||||||
const curPageDebounced = useDebounce(curPage, isLastPage ? 0 : 1000);
|
|
||||||
const [pageToScrollTo, setPageToScrollTo] = useState<number | undefined>(undefined);
|
|
||||||
const { setOverride, setTitle, readerNavBarWidth: currentNavBarWidth } = useContext(NavBarContext);
|
|
||||||
const [retrievingNextChapter, setRetrievingNextChapter] = useState(false);
|
|
||||||
const {
|
|
||||||
data: mangaChaptersData,
|
|
||||||
loading: areChaptersLoading,
|
|
||||||
error: chaptersError,
|
|
||||||
refetch: refetchChapters,
|
|
||||||
} = requestManager.useGetMangaChapters<GetChaptersReaderQuery, GetChaptersReaderQueryVariables>(
|
|
||||||
GET_CHAPTERS_READER,
|
|
||||||
mangaId,
|
|
||||||
{ nextFetchPolicy: 'standby' },
|
|
||||||
);
|
|
||||||
const mangaChapters = mangaChaptersData?.chapters.nodes;
|
|
||||||
|
|
||||||
const isLoading =
|
|
||||||
isMangaLoading ||
|
|
||||||
isChapterLoading ||
|
|
||||||
areChaptersLoading ||
|
|
||||||
arePagesLoading ||
|
|
||||||
(!arePagesUpdatedRef.current && !chapterError && !pagesError);
|
|
||||||
const error = mangaError ?? chapterError ?? chaptersError ?? pagesError;
|
|
||||||
|
|
||||||
const { settings: defaultSettings, loading: areDefaultSettingsLoading } = useDefaultReaderSettings();
|
|
||||||
const [settings, setSettings] = useState(defaultSettings);
|
|
||||||
|
|
||||||
const { settings: metadataSettings } = useMetadataServerSettings();
|
|
||||||
|
|
||||||
const uniqueChapters = useMemo(
|
|
||||||
() =>
|
|
||||||
settings.skipDupChapters
|
|
||||||
? Chapters.removeDuplicates(initialChapterRef.current, mangaChapters ?? [])
|
|
||||||
: (mangaChapters ?? []),
|
|
||||||
[initialChapterRef.current, mangaChapters, settings.skipDupChapters],
|
|
||||||
);
|
|
||||||
const prevChapters = useMemo(
|
|
||||||
() =>
|
|
||||||
Chapters.getNextChapters(chapter, mangaChapters ?? [], {
|
|
||||||
offset: DirectionOffset.PREVIOUS,
|
|
||||||
skipDupe: settings.skipDupChapters,
|
|
||||||
skipDupeChapter: initialChapterRef.current,
|
|
||||||
}),
|
|
||||||
[chapter, initialChapterRef.current, mangaChapters, settings.skipDupChapters],
|
|
||||||
);
|
|
||||||
const nextChapters = useMemo(
|
|
||||||
() =>
|
|
||||||
Chapters.getNextChapters(chapter, mangaChapters ?? [], {
|
|
||||||
skipDupe: settings.skipDupChapters,
|
|
||||||
skipDupeChapter: initialChapterRef.current,
|
|
||||||
}),
|
|
||||||
[chapter, initialChapterRef.current, mangaChapters, settings.skipDupChapters],
|
|
||||||
);
|
|
||||||
const prevChapter = useMemo(
|
|
||||||
() =>
|
|
||||||
Chapters.getNextChapter(chapter, mangaChapters ?? [], {
|
|
||||||
offset: DirectionOffset.PREVIOUS,
|
|
||||||
skipDupe: settings.skipDupChapters,
|
|
||||||
skipDupeChapter: initialChapterRef.current,
|
|
||||||
}),
|
|
||||||
[chapter, initialChapterRef.current, mangaChapters, settings.skipDupChapters],
|
|
||||||
);
|
|
||||||
const nextChapter = useMemo(
|
|
||||||
() =>
|
|
||||||
Chapters.getNextChapter(chapter, mangaChapters ?? [], {
|
|
||||||
skipDupe: settings.skipDupChapters,
|
|
||||||
skipDupeChapter: initialChapterRef.current,
|
|
||||||
}),
|
|
||||||
[chapter, initialChapterRef.current, mangaChapters, settings.skipDupChapters],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateChapter = (patch: UpdateChapterPatchInput) => {
|
|
||||||
if (chapter === fallbackChapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getChapterIdsToDelete = () => {
|
|
||||||
const isAutoDeletionEnabled = !!patch.isRead && !!metadataSettings.deleteChaptersWhileReading;
|
|
||||||
if (!isAutoDeletionEnabled || !mangaChapters) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const chapterToDelete = [chapter, ...prevChapters][metadataSettings.deleteChaptersWhileReading - 1];
|
|
||||||
|
|
||||||
if (!chapterToDelete) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// chapter has to exist in the cache since the reader fetches all chapters of the manga
|
|
||||||
const chapterToDeleteUpToDateData = getChapterFromCache(chapterToDelete.id)!;
|
|
||||||
|
|
||||||
const shouldDeleteChapter =
|
|
||||||
chapterToDeleteUpToDateData.isRead &&
|
|
||||||
Chapters.isDeletable(chapterToDeleteUpToDateData, metadataSettings.deleteChaptersWithBookmark);
|
|
||||||
if (!shouldDeleteChapter) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!settings.skipDupChapters) {
|
|
||||||
return Chapters.getIds([chapterToDelete]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Chapters.getIds(Chapters.addDuplicates([chapterToDelete], mangaChapters));
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadAhead = () => {
|
|
||||||
const currentChapter = getChapterFromCache(chapter.id);
|
|
||||||
|
|
||||||
const inDownloadRange = (patch.lastPageRead ?? 0) / chapter.pageCount > 0.25;
|
|
||||||
const shouldCheckDownloadAhead =
|
|
||||||
isDownloadAheadEnabled && manga.inLibrary && !!currentChapter?.isDownloaded && inDownloadRange;
|
|
||||||
|
|
||||||
if (shouldCheckDownloadAhead) {
|
|
||||||
const nextChapterUpToDate = nextChapter ? getChapterFromCache(nextChapter.id) : null;
|
|
||||||
|
|
||||||
if (!nextChapterUpToDate?.isDownloaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextChaptersUpToDate = Chapters.getNonRead(nextChapters).map(
|
|
||||||
// the chapters have to be in the cache since the reader fetches the whole chapter list of the manga
|
|
||||||
(mangaChapter) => getChapterFromCache(mangaChapter.id)!,
|
|
||||||
);
|
|
||||||
|
|
||||||
const chapterIdsToDownload = nextChaptersUpToDate
|
|
||||||
// "settingsData" can't be undefined since this would not get executed otherwise
|
|
||||||
.slice(-downloadAheadLimit)
|
|
||||||
.filter((mangaChapter) => !mangaChapter.isDownloaded)
|
|
||||||
.map((mangaChapter) => mangaChapter.id)
|
|
||||||
.filter((id) => !Chapters.isDownloading(id));
|
|
||||||
|
|
||||||
if (!chapterIdsToDownload.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Chapters.download(chapterIdsToDownload).catch(
|
|
||||||
defaultPromiseErrorHandler('Reader::updateChapter: shouldDownloadAhead'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
downloadAhead();
|
|
||||||
|
|
||||||
const chapterIds = settings.skipDupChapters
|
|
||||||
? Chapters.getIds(Chapters.addDuplicates([chapter], mangaChapters ?? [chapter]))
|
|
||||||
: [chapter.id];
|
|
||||||
|
|
||||||
requestManager
|
|
||||||
.updateChapters(
|
|
||||||
chapterIds,
|
|
||||||
{
|
|
||||||
...patch,
|
|
||||||
chapterIdsToDelete: getChapterIdsToDelete(),
|
|
||||||
trackProgressMangaId:
|
|
||||||
metadataSettings.updateProgressAfterReading && patch.isRead && manga.trackRecords.totalCount
|
|
||||||
? manga.id
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
{ errorPolicy: 'all' },
|
|
||||||
)
|
|
||||||
.response.catch(defaultPromiseErrorHandler('Reader::updateChapter'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setSettingValue = (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist: boolean = true) => {
|
|
||||||
setSettings({ ...settings, [key]: value });
|
|
||||||
if (persist) {
|
|
||||||
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
|
||||||
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openNextChapter = useCallback(
|
|
||||||
(offset: DirectionOffset) => {
|
|
||||||
const isOpenNextChapter = offset === DirectionOffset.NEXT;
|
|
||||||
const chapterToOpen = isOpenNextChapter ? nextChapter : prevChapter;
|
|
||||||
|
|
||||||
if (!chapterToOpen) {
|
|
||||||
makeToast(
|
|
||||||
t(
|
|
||||||
isOpenNextChapter
|
|
||||||
? 'reader.error.label.next_chapter_does_not_exist'
|
|
||||||
: 'reader.error.label.prev_chapter_does_not_exist',
|
|
||||||
),
|
|
||||||
'error',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRetrievingNextChapter(true);
|
|
||||||
setCurPage(0);
|
|
||||||
|
|
||||||
navigate(`/manga/${manga.id}/chapter/${chapterToOpen.sourceOrder}`, {
|
|
||||||
replace: true,
|
|
||||||
state: location.state,
|
|
||||||
});
|
|
||||||
|
|
||||||
setRetrievingNextChapter(false);
|
|
||||||
},
|
|
||||||
[manga.id, prevChapter?.id, nextChapter?.id],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoading || !chapter) {
|
|
||||||
setCurPage(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setWasLastPageReadSet(true);
|
|
||||||
if (chapter.lastPageRead === chapter.pageCount - 1) {
|
|
||||||
// last page, also probably read = true, we will load the first page.
|
|
||||||
setCurPage(0);
|
|
||||||
} else setCurPage(chapter.lastPageRead);
|
|
||||||
}, [chapter, isLoading]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!manga?.title || chapter.name === t('global.label.loading')) {
|
|
||||||
setTitle(t('reader.title'));
|
|
||||||
} else {
|
|
||||||
setTitle(`${manga.title}: ${chapter.name}`);
|
|
||||||
}
|
|
||||||
}, [t, manga, chapter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!areDefaultSettingsLoading && !isMangaLoading) {
|
|
||||||
setSettings(getReaderSettingsFor(manga, defaultSettings));
|
|
||||||
}
|
|
||||||
}, [areDefaultSettingsLoading, isMangaLoading]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
// set the custom navbar
|
|
||||||
setOverride({
|
|
||||||
status: true,
|
|
||||||
value: (
|
|
||||||
<ReaderNavBar
|
|
||||||
settings={settings}
|
|
||||||
setSettingValue={setSettingValue}
|
|
||||||
manga={manga}
|
|
||||||
chapter={chapter}
|
|
||||||
chapters={uniqueChapters}
|
|
||||||
curPage={curPage}
|
|
||||||
scrollToPage={setPageToScrollTo}
|
|
||||||
openNextChapter={openNextChapter}
|
|
||||||
retrievingNextChapter={retrievingNextChapter}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
// clean up for when we leave the reader
|
|
||||||
return () => setOverride({ status: false, value: <div /> });
|
|
||||||
}, [manga, chapter, settings, curPage, chapterIndex, retrievingNextChapter, openNextChapter, uniqueChapters]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!wasLastPageReadSet) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chapter === fallbackChapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chapterUpToDate = getChapterFromCache(chapter.id);
|
|
||||||
if (curPageDebounced === chapterUpToDate?.lastPageRead) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do not mutate the chapter, this will cause the page to jump around due to always scrolling to the last read page
|
|
||||||
const updateLastPageRead = curPageDebounced !== -1;
|
|
||||||
const updateIsRead = curPageDebounced === chapter.pageCount - 1;
|
|
||||||
const shouldUpdateChapter = updateLastPageRead || updateIsRead;
|
|
||||||
if (!shouldUpdateChapter) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateChapter({
|
|
||||||
lastPageRead: updateLastPageRead ? curPageDebounced : undefined,
|
|
||||||
isRead: updateIsRead ? true : undefined,
|
|
||||||
});
|
|
||||||
}, [curPageDebounced, isDownloadAheadEnabled]);
|
|
||||||
|
|
||||||
const loadNextChapter = useCallback(() => {
|
|
||||||
updateChapter({
|
|
||||||
lastPageRead: chapter.pageCount - 1,
|
|
||||||
isRead: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
openNextChapter(DirectionOffset.NEXT);
|
|
||||||
}, [chapter.pageCount, openNextChapter, chapter, manga]);
|
|
||||||
|
|
||||||
const loadPrevChapter = useCallback(() => {
|
|
||||||
openNextChapter(DirectionOffset.PREVIOUS);
|
|
||||||
}, [openNextChapter]);
|
|
||||||
|
|
||||||
const scrollbarHeight = MediaQuery.useGetScrollbarSize('height');
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
height: '100vh',
|
|
||||||
width: '100vw',
|
|
||||||
display: 'grid',
|
|
||||||
placeItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CircularProgress thickness={5} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<EmptyViewAbsoluteCentered
|
|
||||||
message={t('global.error.label.failed_to_load_data')}
|
|
||||||
messageExtra={error.message}
|
|
||||||
retry={() => {
|
|
||||||
if (mangaError) {
|
|
||||||
refetchManga().catch(defaultPromiseErrorHandler('Reader::refetchManga'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chapterError) {
|
|
||||||
fetchChapter().catch(defaultPromiseErrorHandler('Reader::refetchChapter'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chaptersError) {
|
|
||||||
refetchChapters().catch(defaultPromiseErrorHandler('Reader::refetchChapters'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pagesError) {
|
|
||||||
doFetchPages();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chapter.pageCount) {
|
|
||||||
return <EmptyViewAbsoluteCentered message={t('reader.error.label.no_pages_found')} retry={doFetchPages} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pages = range(chapter.pageCount).map((index) => ({
|
|
||||||
index,
|
|
||||||
src: requestManager.getChapterPageUrl(mangaId, chapterIndex, index),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ReaderComponent = getReaderComponent(settings.readerType);
|
|
||||||
|
|
||||||
// last page, also probably read = true, we will load the first page.
|
|
||||||
const initialPage = pageToScrollTo ?? (chapter.lastPageRead === chapter.pageCount - 1 ? 0 : chapter.lastPageRead);
|
|
||||||
|
|
||||||
const navBarWidth = settings.staticNav ? currentNavBarWidth : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
minWidth: `calc((100vw - (100vw - 100%)) - ${navBarWidth}px)`, // 100vw = width excluding scrollbar; 100% = width including scrollbar
|
|
||||||
minHeight: `calc(100vh - ${scrollbarHeight}px)`,
|
|
||||||
marginLeft: `${navBarWidth}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PageNumber settings={settings} curPage={curPage} pageCount={chapter.pageCount} />
|
|
||||||
<Box sx={{ alignSelf: 'stretch' }}>
|
|
||||||
<ReaderComponent
|
|
||||||
key={chapter.id}
|
|
||||||
pages={pages}
|
|
||||||
pageCount={chapter.pageCount}
|
|
||||||
setCurPage={setCurPage}
|
|
||||||
initialPage={initialPage}
|
|
||||||
curPage={curPage}
|
|
||||||
settings={settings}
|
|
||||||
manga={manga}
|
|
||||||
chapter={chapter}
|
|
||||||
nextChapter={loadNextChapter}
|
|
||||||
prevChapter={loadPrevChapter}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user