Delete deprecated reader files

This commit is contained in:
schroda
2024-10-26 16:07:14 +02:00
parent 484d6cdac4
commit 8e59763bba
16 changed files with 3 additions and 2251 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
});

View File

@@ -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>
);
});

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}