Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verticall scroll navigation and fix #200

Merged
merged 6 commits into from
Nov 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 2 additions & 36 deletions src/components/reader/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 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 React, { useEffect, useRef } from 'react';
import React, { useRef } from 'react';
import SpinnerImage from 'components/util/SpinnerImage';
import useLocalStorage from 'util/useLocalStorage';
import Box from '@mui/system/Box';
Expand Down Expand Up @@ -57,52 +57,18 @@ interface IProps {
src: string
index: number
onImageLoad: () => void
setCurPage: React.Dispatch<React.SetStateAction<number>>
settings: IReaderSettings
}

const Page = React.forwardRef((props: IProps, ref: any) => {
const {
src, index, onImageLoad, setCurPage, settings,
src, index, onImageLoad, settings,
} = props;

const [useCache] = useLocalStorage<boolean>('useCache', true);

const imgRef = useRef<HTMLImageElement>(null);

const handleVerticalScroll = () => {
if (imgRef.current) {
const rect = imgRef.current.getBoundingClientRect();
if (rect.y < 0 && rect.y + rect.height > 0) {
setCurPage(index);
}
}
};

const handleHorizontalScroll = () => {
if (imgRef.current) {
const rect = imgRef.current.getBoundingClientRect();
if (rect.left <= window.innerWidth / 2 && rect.right > window.innerWidth / 2) {
setCurPage(index);
}
}
};

useEffect(() => {
switch (settings.readerType) {
case 'Webtoon':
case 'ContinuesVertical':
window.addEventListener('scroll', handleVerticalScroll);
return () => window.removeEventListener('scroll', handleVerticalScroll);
case 'ContinuesHorizontalLTR':
case 'ContinuesHorizontalRTL':
window.addEventListener('scroll', handleHorizontalScroll);
return () => window.removeEventListener('scroll', handleHorizontalScroll);
default:
return () => {};
}
}, [handleVerticalScroll]);

const imgStyle = imageStyle(settings);

return (
Expand Down
1 change: 0 additions & 1 deletion src/components/reader/pager/DoublePagedPager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export default function DoublePagedPager(props: IReaderProps) {
index={curPage}
src={(pagesDisplayed.current === 1) ? pages[curPage].src : ''}
onImageLoad={() => {}}
setCurPage={setCurPage}
settings={settings}
/>,
document.getElementById('display'),
Expand Down
51 changes: 46 additions & 5 deletions src/components/reader/pager/HorizontalPager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,26 @@ import React, { useEffect, useRef } from 'react';
import { Box } from '@mui/system';
import Page from '../Page';

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 isAtEnd = () => window.innerWidth + window.scrollX >= document.body.scrollWidth;
const isAtStart = () => window.scrollX <= 0;

export default function HorizontalPager(props: IReaderProps) {
const {
pages, curPage, settings, setCurPage, prevChapter, nextChapter,
pages, curPage, initialPage, settings, setCurPage, prevChapter, nextChapter,
} = props;

const currentPageRef = useRef(initialPage);
const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLDivElement[]>([]);

Expand Down Expand Up @@ -87,9 +102,12 @@ export default function HorizontalPager(props: IReaderProps) {
};

useEffect(() => {
// scroll last read page into view after first mount
pagesRef.current[curPage]?.scrollIntoView({ inline: 'center' });
}, [pagesRef.current.length]);
// Delay scrolling to next cycle
setTimeout(() => {
// scroll last read page into view when initialPage changes
pagesRef.current[initialPage]?.scrollIntoView({ inline: 'center' });
}, 0);
}, [initialPage]);

useEffect(() => {
selfRef.current?.addEventListener('mousedown', dragControl);
Expand All @@ -113,6 +131,30 @@ export default function HorizontalPager(props: IReaderProps) {
};
}, [selfRef, curPage]);

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/Tachidesk-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}
Expand All @@ -134,7 +176,6 @@ export default function HorizontalPager(props: IReaderProps) {
index={page.index}
src={page.src}
onImageLoad={() => {}}
setCurPage={setCurPage}
settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
/>
Expand Down
1 change: 0 additions & 1 deletion src/components/reader/pager/PagedPager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ export default function PagedReader(props: IReaderProps) {
index={curPage}
onImageLoad={() => {}}
src={pages[curPage].src}
setCurPage={setCurPage}
settings={settings}
/>
</Box>
Expand Down
148 changes: 86 additions & 62 deletions src/components/reader/pager/VerticalPager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,92 +5,116 @@
* 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 React, { useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Box } from '@mui/system';
import Page from '../Page';

export default function VerticalReader(props: IReaderProps) {
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_OFFSET = 0.95;
const SCROLL_BEHAVIOR: ScrollBehavior = 'smooth';

const isAtBottom = () => window.innerHeight + window.scrollY >= document.body.offsetHeight;
const isAtTop = () => window.scrollY <= 0;

export default function VerticalPager(props: IReaderProps) {
const {
pages, settings, setCurPage, curPage, nextChapter, prevChapter,
pages, settings, setCurPage, initialPage, nextChapter, prevChapter,
} = props;

const currentPageRef = useRef(initialPage);
const selfRef = useRef<HTMLDivElement>(null);
const pagesRef = useRef<HTMLDivElement[]>([]);

useEffect(() => {
pagesRef.current = pagesRef.current.slice(0, pages.length);
}, [pages.length]);

function nextPage() {
if (curPage < pages.length - 1) {
pagesRef.current[curPage + 1]?.scrollIntoView();
setCurPage((page) => page + 1);
} else if (settings.loadNextonEnding) {
nextChapter();
}
}
const handleScroll = () => {
if (!selfRef.current) return;

if (isAtBottom()) {
// 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/Tachidesk-WebUI/issues/14 for more info
currentPageRef.current = pages.length - 1;
setCurPage(currentPageRef.current);

function prevPage() {
if (curPage > 0) {
const rect = pagesRef.current[curPage].getBoundingClientRect();
if (rect.y < 0 && rect.y + rect.height > 0) {
pagesRef.current[curPage]?.scrollIntoView();
// Go to next chapter if configured to and at bottom
if (settings.loadNextonEnding) {
nextChapter();
}
} else {
pagesRef.current[curPage - 1]?.scrollIntoView();
setCurPage(curPage - 1);
// Update current page in parent
const currentPage = findCurrentPageIndex(selfRef.current);
if (currentPage !== currentPageRef.current) {
currentPageRef.current = currentPage;
setCurPage(currentPage);
}
}
} else if (curPage === 0) {
prevChapter();
}
}
};

function keyboardControl(e:KeyboardEvent) {
switch (e.code) {
case 'Space':
e.preventDefault();
nextPage();
break;
case 'ArrowRight':
nextPage();
break;
case 'ArrowLeft':
prevPage();
break;
default:
break;
}
}
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [settings.loadNextonEnding]);

function clickControl(e:MouseEvent) {
if (e.clientX > window.innerWidth / 2) {
nextPage();
} else {
prevPage();
const go = useCallback((direction: 'up' | 'down') => {
if (direction === 'down' && isAtBottom()) {
nextChapter();
return;
}
}

const handleLoadNextonEnding = () => {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
nextChapter();
if (direction === 'up' && isAtTop()) {
prevChapter();
return;
}
};

window.scroll({
top: window.scrollY + (window.innerHeight * SCROLL_OFFSET) * (direction === 'up' ? -1 : 1),
behavior: SCROLL_BEHAVIOR,
});
}, [nextChapter, prevChapter]);

useEffect(() => {
if (settings.loadNextonEnding) { document.addEventListener('scroll', handleLoadNextonEnding); }
document.addEventListener('keydown', keyboardControl, false);
selfRef.current?.addEventListener('click', clickControl);
const handleKeyboard = (e:KeyboardEvent) => {
switch (e.code) {
case 'Space':
case 'ArrowRight':
e.preventDefault();
go('down');
break;
case 'ArrowLeft':
e.preventDefault();
go('up');
break;
default:
break;
}
};

document.addEventListener('keydown', handleKeyboard, false);
return () => {
document.removeEventListener('scroll', handleLoadNextonEnding);
document.removeEventListener('keydown', keyboardControl);
selfRef.current?.removeEventListener('click', clickControl);
document.removeEventListener('keydown', handleKeyboard);
};
}, [selfRef, curPage]);
}, []);

useEffect(() => {
// scroll last read page into view after first mount
pagesRef.current[curPage].scrollIntoView();
}, [pagesRef.current.length]);
// Delay scrolling to next cycle
setTimeout(() => {
// scroll last read page into view when initialPage changes
pagesRef.current[initialPage]?.scrollIntoView();
}, 0);
}, [initialPage]);

return (
<Box
Expand All @@ -102,6 +126,7 @@ export default function VerticalReader(props: IReaderProps) {
margin: '0 auto',
width: '100%',
}}
onClick={(e) => go(e.clientX > window.innerWidth / 2 ? 'down' : 'up')}
>
{
pages.map((page) => (
Expand All @@ -110,7 +135,6 @@ export default function VerticalReader(props: IReaderProps) {
index={page.index}
src={page.src}
onImageLoad={() => {}}
setCurPage={setCurPage}
settings={settings}
ref={(e:HTMLDivElement) => { pagesRef.current[page.index] = e; }}
/>
Expand Down
Loading