Skip to content

Commit

Permalink
Verticall scroll navigation and fix (#200)
Browse files Browse the repository at this point in the history
* Remove scroll tracking from Page component

* Replace initial scrolling trigger with initialPage prop to be more clear on when to scroll to inital page

* Add current page tracking back to HorizontalPager

* Memoize prevChapter and nextChapter functions

* Rewrite navigation in vertical pager to scroll by screen percentage instead of whole pages, refactor code

* Handle last pages explicitly to fix issue with small last pages
  • Loading branch information
martinek authored Nov 24, 2022
1 parent 31a3679 commit 7aa4d2f
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 125 deletions.
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 @@ -105,7 +105,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

0 comments on commit 7aa4d2f

Please sign in to comment.