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 (
+
+
+
+
+ );
+});
+
+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)}
>
- {/*
{/*
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{