/* * 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'; import { IReaderProps } from '@/typings'; import { Page } from '@/components/reader/Page'; 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_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 { pages, settings, setCurPage, initialPage, nextChapter, prevChapter } = props; const currentPageRef = useRef(initialPage); const selfRef = useRef(null); const pagesRef = useRef([]); 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/Tachidesk-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(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') => { if (direction === 'down' && isAtBottom()) { nextChapter(); return; } 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(() => { const handleKeyboard = (e: KeyboardEvent) => { switch (e.key) { case 'Space': e.preventDefault(); go(e.shiftKey ? 'up' : 'down'); break; case 'ArrowDown': case 'ArrowRight': e.preventDefault(); go(e.shiftKey ? 'up' : 'down'); break; case 'ArrowUp': case 'ArrowLeft': e.preventDefault(); go(e.shiftKey ? 'down' : 'up'); break; default: break; } }; document.addEventListener('keydown', handleKeyboard, false); return () => { document.removeEventListener('keydown', handleKeyboard); }; }, [go]); useEffect(() => { // Delay scrolling to next cycle setTimeout(() => { // scroll last read page into view when initialPage changes pagesRef.current[initialPage]?.scrollIntoView(); }, 0); }, [initialPage]); return ( { if (didMouseMove.current) { didMouseMove.current = false; return; } go(e.clientX > window.innerWidth / 2 ? 'down' : 'up'); }} > {pages.map((page) => ( {}} settings={settings} ref={(e: HTMLDivElement) => { pagesRef.current[page.index] = e; }} /> ))} ); }