Delete deprecated reader files
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user