Delete deprecated reader files
This commit is contained in:
@@ -12,7 +12,7 @@ import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSe
|
||||
import { useManageMangaLibraryState } from '@/modules/manga/hooks/useManageMangaLibraryState.tsx';
|
||||
import { ChapterType, MangaReaderFieldsFragment, MangaType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
||||
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';
|
||||
|
||||
export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate';
|
||||
|
||||
@@ -17,7 +17,6 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
||||
skipDupChapters: undefined,
|
||||
fitPageToWindow: undefined,
|
||||
scalePage: undefined,
|
||||
readerType: undefined,
|
||||
offsetFirstPage: undefined,
|
||||
migration: undefined,
|
||||
deleteChaptersManuallyMarkedRead: undefined,
|
||||
|
||||
@@ -15,7 +15,6 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
||||
skipDupChapters: true,
|
||||
fitPageToWindow: true,
|
||||
scalePage: false,
|
||||
readerType: 'ContinuesVertical',
|
||||
offsetFirstPage: false,
|
||||
readerWidth: 50,
|
||||
};
|
||||
|
||||
@@ -6,20 +6,6 @@
|
||||
* 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 {
|
||||
staticNav: boolean;
|
||||
showPageNumber: boolean;
|
||||
@@ -27,25 +13,6 @@ export interface IReaderSettings {
|
||||
skipDupChapters: boolean;
|
||||
fitPageToWindow: boolean;
|
||||
scalePage: boolean;
|
||||
readerType: ReaderType;
|
||||
offsetFirstPage: boolean;
|
||||
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