From 1b122d115713c25ac449df10d14d6f20422e98df Mon Sep 17 00:00:00 2001 From: Manchewable <35658388+Manchewable@users.noreply.github.com> Date: Fri, 28 May 2021 05:36:55 -0700 Subject: [PATCH] Add a Double Page Viewer (#105) * add double page reader * implement singleRTL * add on image load handler * add retry display time interval * remove comments * add double page wrapper * fix image getting out of bounds * remove comments * remove unused styles * return imageStyle as type CSSProperties * rename DoublePagedReader to DoublePagedPager --- .../components/manga/reader/DoublePage.tsx | 62 +++++ .../src/components/manga/reader/Page.tsx | 46 +++- .../manga/reader/pager/DoublePagedPager.tsx | 223 ++++++++++++++++++ .../manga/reader/pager/HorizontalPager.tsx | 1 + .../manga/reader/pager/PagedPager.tsx | 33 ++- .../manga/reader/pager/VerticalPager.tsx | 1 + .../src/components/navbar/ReaderNavBar.tsx | 16 +- webUI/react/src/screens/manga/Reader.tsx | 16 +- webUI/react/src/typings.d.ts | 3 + 9 files changed, 373 insertions(+), 28 deletions(-) create mode 100644 webUI/react/src/components/manga/reader/DoublePage.tsx create mode 100644 webUI/react/src/components/manga/reader/pager/DoublePagedPager.tsx diff --git a/webUI/react/src/components/manga/reader/DoublePage.tsx b/webUI/react/src/components/manga/reader/DoublePage.tsx new file mode 100644 index 0000000..7cca28b --- /dev/null +++ b/webUI/react/src/components/manga/reader/DoublePage.tsx @@ -0,0 +1,62 @@ +/* + * 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 { makeStyles } from '@material-ui/core/styles'; +import React from 'react'; + +const useStyles = (settings: IReaderSettings) => makeStyles({ + image: { + display: 'block', + marginBottom: 0, + width: 'auto', + minHeight: '99vh', + height: 'auto', + maxHeight: '99vh', + objectFit: 'contain', + }, + page: { + display: 'flex', + flexDirection: settings.readerType === 'DoubleLTR' ? 'row' : 'row-reverse', + justifyContent: 'center', + margin: '0 auto', + width: 'auto', + height: 'auto', + overflowX: 'scroll', + }, +}); + +interface IProps { + index: number + image1src: string + image2src: string + settings: IReaderSettings +} + +const DoublePage = React.forwardRef((props: IProps, ref: any) => { + const { + image1src, image2src, index, settings, + } = props; + + const classes = useStyles(settings)(); + + return ( +
+ {`Page + {`Page +
+ ); +}); + +export default DoublePage; diff --git a/webUI/react/src/components/manga/reader/Page.tsx b/webUI/react/src/components/manga/reader/Page.tsx index 5aa1176..155a303 100644 --- a/webUI/react/src/components/manga/reader/Page.tsx +++ b/webUI/react/src/components/manga/reader/Page.tsx @@ -7,8 +7,31 @@ import CircularProgress from '@material-ui/core/CircularProgress'; import { makeStyles } from '@material-ui/core/styles'; +import { CSSProperties } from '@material-ui/core/styles/withStyles'; import React, { useEffect, useRef, useState } from 'react'; +function imageStyle(settings: IReaderSettings): CSSProperties { + if (settings.readerType === 'DoubleLTR' || settings.readerType === 'DoubleRTL') { + return { + display: 'block', + marginBottom: 0, + width: 'auto', + minHeight: '99vh', + height: 'auto', + maxHeight: '99vh', + objectFit: 'contain', + }; + } + + return { + display: 'block', + marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0, + minWidth: '50vw', + width: '100%', + maxWidth: '100%', + }; +} + const useStyles = (settings: IReaderSettings) => makeStyles({ loading: { margin: '100px auto', @@ -22,25 +45,20 @@ const useStyles = (settings: IReaderSettings) => makeStyles({ backgroundColor: '#525252', marginBottom: 10, }, - image: { - display: 'block', - marginBottom: settings.readerType === 'ContinuesVertical' ? '15px' : 0, - minWidth: '50vw', - width: '100%', - maxWidth: '100%', - }, + image: imageStyle(settings), }); interface IProps { src: string index: number + onImageLoad: () => void setCurPage: React.Dispatch> settings: IReaderSettings } function LazyImage(props: IProps) { const { - src, index, setCurPage, settings, + src, index, onImageLoad, setCurPage, settings, } = props; const classes = useStyles(settings)(); @@ -70,7 +88,14 @@ function LazyImage(props: IProps) { const img = new Image(); img.src = src; - img.onload = () => setImagsrc(src); + img.onload = () => { + setImagsrc(src); + onImageLoad(); + }; + + return () => { + img.onload = null; + }; }, [src]); if (imageSrc.length === 0) { @@ -93,7 +118,7 @@ function LazyImage(props: IProps) { const Page = React.forwardRef((props: IProps, ref: any) => { const { - src, index, setCurPage, settings, + src, index, onImageLoad, setCurPage, settings, } = props; return ( @@ -101,6 +126,7 @@ const Page = React.forwardRef((props: IProps, ref: any) => { diff --git a/webUI/react/src/components/manga/reader/pager/DoublePagedPager.tsx b/webUI/react/src/components/manga/reader/pager/DoublePagedPager.tsx new file mode 100644 index 0000000..0c19d18 --- /dev/null +++ b/webUI/react/src/components/manga/reader/pager/DoublePagedPager.tsx @@ -0,0 +1,223 @@ +/* + * 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 { makeStyles } from '@material-ui/core/styles'; +import React, { useEffect, useRef } from 'react'; +import ReactDOM from 'react-dom'; +import Page from '../Page'; +import DoublePage from '../DoublePage'; + +const useStyles = (settings: IReaderSettings) => makeStyles({ + reader: { + display: 'flex', + flexDirection: (settings.readerType === 'DoubleLTR') ? 'row' : 'row-reverse', + justifyContent: 'center', + margin: '0 auto', + width: 'auto', + height: 'auto', + overflowX: 'scroll', + }, +}); + +export default function DoublePagedPager(props: IReaderProps) { + const { + pages, settings, setCurPage, curPage, nextChapter, prevChapter, + } = props; + + const classes = useStyles(settings)(); + + const selfRef = useRef(null); + const pagesRef = useRef([]); + + const pagesDisplayed = useRef(0); + const pageLoaded = useRef(Array(pages.length).fill(false)); + + function pagesToGoBack() { + for (let i = 1; i <= 2; i++) { + if (curPage - i > 0 && pagesRef.current[curPage - i]) { + if (pagesRef.current[curPage - i].children[0] instanceof HTMLImageElement) { + const imgElem = pagesRef.current[curPage - i].children[0] as HTMLImageElement; + const aspectRatio = imgElem.height / imgElem.width; + if (aspectRatio < 1) { + return 1; + } + } + } + } + return 2; + } + + function nextPage() { + if (curPage < pages.length - 1) { + const nextCurPage = curPage + pagesDisplayed.current; + setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage); + } else if (settings.loadNextonEnding) { + nextChapter(); + } + } + + function prevPage() { + if (curPage > 0) { + const nextCurPage = curPage - pagesToGoBack(); + setCurPage((nextCurPage < 0) ? 0 : nextCurPage); + } else { + prevChapter(); + } + } + + function goLeft() { + if (settings.readerType === 'DoubleLTR') { + prevPage(); + } else { + nextPage(); + } + } + + function goRight() { + if (settings.readerType === 'DoubleLTR') { + nextPage(); + } else { + prevPage(); + } + } + + function setPagesToDisplay() { + pagesDisplayed.current = 0; + if (curPage < pages.length && pagesRef.current[curPage]) { + if (pagesRef.current[curPage].children[0] instanceof HTMLImageElement) { + pagesDisplayed.current = 1; + const imgElem = pagesRef.current[curPage].children[0] as HTMLImageElement; + const aspectRatio = imgElem.height / imgElem.width; + if (aspectRatio < 1) { + return; + } + } + } + if (curPage + 1 < pages.length && pagesRef.current[curPage + 1]) { + if (pagesRef.current[curPage + 1].children[0] instanceof HTMLImageElement) { + const imgElem = pagesRef.current[curPage + 1].children[0] as HTMLImageElement; + const aspectRatio = imgElem.height / imgElem.width; + if (aspectRatio < 1) { + return; + } + pagesDisplayed.current = 2; + } + } + } + + function showPages() { + if (pagesDisplayed.current === 2) { + ReactDOM.render( + , + document.getElementById('display'), + ); + } else if (pagesDisplayed.current === 1) { + ReactDOM.render( + {}} + setCurPage={setCurPage} + settings={settings} + />, + document.getElementById('display'), + ); + } else { + ReactDOM.render( +
, + document.getElementById('display'), + ); + } + } + + function keyboardControl(e:KeyboardEvent) { + switch (e.code) { + 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(); + } + } + + function handleImageLoad(index: number) { + return () => { + pageLoaded.current[index] = true; + }; + } + + useEffect(() => { + pagesRef.current.forEach((e) => { + const pageRef = e; + pageRef.style.display = 'none'; + }); + }, []); + + useEffect(() => { + const retryDisplay = setInterval(() => { + const isLastPage = (curPage === pages.length - 1); + if ((!isLastPage && pageLoaded.current[curPage] && pageLoaded.current[curPage + 1]) + || pageLoaded.current[curPage]) { + setPagesToDisplay(); + showPages(); + clearInterval(retryDisplay); + } + }, 50); + + document.addEventListener('keydown', keyboardControl); + selfRef.current?.addEventListener('click', clickControl); + + return () => { + clearInterval(retryDisplay); + document.removeEventListener('keydown', keyboardControl); + selfRef.current?.removeEventListener('click', clickControl); + }; + }, [selfRef, curPage, settings.readerType]); + + return ( +
+
+ { + pages.map((page) => ( + { pagesRef.current[page.index] = e; }} + /> + )) + } +
+
+
+ ); +} diff --git a/webUI/react/src/components/manga/reader/pager/HorizontalPager.tsx b/webUI/react/src/components/manga/reader/pager/HorizontalPager.tsx index 81a2973..b33a98d 100644 --- a/webUI/react/src/components/manga/reader/pager/HorizontalPager.tsx +++ b/webUI/react/src/components/manga/reader/pager/HorizontalPager.tsx @@ -40,6 +40,7 @@ export default function HorizontalPager(props: IProps) { key={page.index} index={page.index} src={page.src} + onImageLoad={() => {}} setCurPage={setCurPage} settings={settings} /> diff --git a/webUI/react/src/components/manga/reader/pager/PagedPager.tsx b/webUI/react/src/components/manga/reader/pager/PagedPager.tsx index e23c187..bbbc609 100644 --- a/webUI/react/src/components/manga/reader/pager/PagedPager.tsx +++ b/webUI/react/src/components/manga/reader/pager/PagedPager.tsx @@ -38,7 +38,27 @@ export default function PagedReader(props: IReaderProps) { } function prevPage() { - if (curPage > 0) { setCurPage(curPage - 1); } else if (curPage === 0) { prevChapter(); } + if (curPage > 0) { + setCurPage(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) { @@ -48,10 +68,10 @@ export default function PagedReader(props: IReaderProps) { nextPage(); break; case 'ArrowRight': - nextPage(); + goRight(); break; case 'ArrowLeft': - prevPage(); + goLeft(); break; default: break; @@ -60,9 +80,9 @@ export default function PagedReader(props: IReaderProps) { function clickControl(e:MouseEvent) { if (e.clientX > window.innerWidth / 2) { - nextPage(); + goRight(); } else { - prevPage(); + goLeft(); } } @@ -74,13 +94,14 @@ export default function PagedReader(props: IReaderProps) { document.removeEventListener('keydown', keyboardControl); selfRef.current?.removeEventListener('click', clickControl); }; - }, [selfRef, curPage]); + }, [selfRef, curPage, settings.readerType]); return (
{}} src={pages[curPage].src} setCurPage={setCurPage} settings={settings} diff --git a/webUI/react/src/components/manga/reader/pager/VerticalPager.tsx b/webUI/react/src/components/manga/reader/pager/VerticalPager.tsx index 7a47f2d..5378e79 100644 --- a/webUI/react/src/components/manga/reader/pager/VerticalPager.tsx +++ b/webUI/react/src/components/manga/reader/pager/VerticalPager.tsx @@ -114,6 +114,7 @@ export default function VerticalReader(props: IReaderProps) { key={page.index} index={page.index} src={page.src} + onImageLoad={() => {}} setCurPage={setCurPage} settings={settings} ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }} diff --git a/webUI/react/src/components/navbar/ReaderNavBar.tsx b/webUI/react/src/components/navbar/ReaderNavBar.tsx index 8dc4393..2976e22 100644 --- a/webUI/react/src/components/navbar/ReaderNavBar.tsx +++ b/webUI/react/src/components/navbar/ReaderNavBar.tsx @@ -286,17 +286,25 @@ export default function ReaderNavBar(props: IProps) { onChange={(e) => setSettingValue('readerType', e.target.value)} > - Left to right + Single Page (LTR) - {/* - Right to left(WIP) + + Single Page (RTL) - */} + {/* Vertical(WIP) */} + + Double Page (LTR) + + + + Double Page (RTL) + + Webtoon diff --git a/webUI/react/src/screens/manga/Reader.tsx b/webUI/react/src/screens/manga/Reader.tsx index c6dde33..e468d67 100644 --- a/webUI/react/src/screens/manga/Reader.tsx +++ b/webUI/react/src/screens/manga/Reader.tsx @@ -11,7 +11,8 @@ import React, { useContext, useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import HorizontalPager from 'components/manga/reader/pager/HorizontalPager'; import PageNumber from 'components/manga/reader/PageNumber'; -import WebtoonPager from 'components/manga/reader/pager/PagedPager'; +import PagedPager from 'components/manga/reader/pager/PagedPager'; +import DoublePagedPager from 'components/manga/reader/pager/DoublePagedPager'; import VerticalPager from 'components/manga/reader/pager/VerticalPager'; import ReaderNavBar, { defaultReaderSettings } from 'components/navbar/ReaderNavBar'; import NavbarContext from 'context/NavbarContext'; @@ -32,19 +33,18 @@ const useStyles = (settings: IReaderSettings) => makeStyles({ const getReaderComponent = (readerType: ReaderType) => { switch (readerType) { case 'ContinuesVertical': - return VerticalPager; - break; case 'Webtoon': return VerticalPager; break; case 'SingleVertical': - return WebtoonPager; - break; case 'SingleRTL': - return WebtoonPager; - break; case 'SingleLTR': - return WebtoonPager; + return PagedPager; + break; + case 'DoubleVertical': + case 'DoubleRTL': + case 'DoubleLTR': + return DoublePagedPager; break; case 'ContinuesHorizontal': return HorizontalPager; diff --git a/webUI/react/src/typings.d.ts b/webUI/react/src/typings.d.ts index f70c485..99ec207 100644 --- a/webUI/react/src/typings.d.ts +++ b/webUI/react/src/typings.d.ts @@ -114,6 +114,9 @@ type ReaderType = 'SingleVertical' | 'SingleRTL' | 'SingleLTR' | +'DoubleVertical' | +'DoubleRTL' | +'DoubleLTR' | 'ContinuesHorizontal'; interface IReaderSettings{