Fix click/hotkey scrolling

- properly handle theme direction and reading direction combination
- prevent opening previous/next chapter in non-continuous reading modes
This commit is contained in:
schroda
2024-12-08 22:28:00 +01:00
parent be0d1dbf30
commit 8491091b2b
2 changed files with 81 additions and 35 deletions

View File

@@ -8,6 +8,7 @@
import { useHotkeys as useHotKeysHook, useHotkeysContext } from 'react-hotkeys-hook'; import { useHotkeys as useHotKeysHook, useHotkeysContext } from 'react-hotkeys-hook';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useTheme } from '@mui/material/styles';
import { HOTKEY_SCOPES } from '@/modules/hotkeys/Hotkeys.constants.ts'; import { HOTKEY_SCOPES } from '@/modules/hotkeys/Hotkeys.constants.ts';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ReaderHotkey, ReadingMode } from '@/modules/reader/types/Reader.types.ts'; import { ReaderHotkey, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
@@ -42,6 +43,7 @@ export const ReaderHotkeys = ({
}: { }: {
scrollElementRef: React.MutableRefObject<HTMLElement | null>; scrollElementRef: React.MutableRefObject<HTMLElement | null>;
}) => { }) => {
const { direction: themeDirection } = useTheme();
const { enableScope, disableScope } = useHotkeysContext(); const { enableScope, disableScope } = useHotkeysContext();
const { manga } = useReaderStateMangaContext(); const { manga } = useReaderStateMangaContext();
const { isVisible, setIsVisible } = useReaderOverlayContext(); const { isVisible, setIsVisible } = useReaderOverlayContext();
@@ -63,13 +65,15 @@ export const ReaderHotkeys = ({
ReaderControls.scroll( ReaderControls.scroll(
ScrollOffset.BACKWARD, ScrollOffset.BACKWARD,
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value], CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value],
readingMode.value,
readingDirection.value, readingDirection.value,
themeDirection,
scrollElementRef.current, scrollElementRef.current,
openChapter, openChapter,
ReaderScrollAmount.SMALL, ReaderScrollAmount.SMALL,
), ),
{ preventDefault: true }, { preventDefault: true },
[readingMode.value, readingDirection.value, openChapter], [readingMode.value, readingDirection.value, themeDirection, openChapter],
); );
useHotkeys( useHotkeys(
hotkeys[ReaderHotkey.SCROLL_FORWARD], hotkeys[ReaderHotkey.SCROLL_FORWARD],
@@ -78,13 +82,15 @@ export const ReaderHotkeys = ({
ReaderControls.scroll( ReaderControls.scroll(
ScrollOffset.FORWARD, ScrollOffset.FORWARD,
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value], CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value],
readingMode.value,
readingDirection.value, readingDirection.value,
themeDirection,
scrollElementRef.current, scrollElementRef.current,
openChapter, openChapter,
ReaderScrollAmount.SMALL, ReaderScrollAmount.SMALL,
), ),
{ preventDefault: true }, { preventDefault: true },
[readingMode.value, readingDirection.value, openChapter], [readingMode.value, readingDirection.value, themeDirection, openChapter],
); );
useHotkeys(hotkeys[ReaderHotkey.PREVIOUS_CHAPTER], () => openChapter('previous'), [openChapter]); useHotkeys(hotkeys[ReaderHotkey.PREVIOUS_CHAPTER], () => openChapter('previous'), [openChapter]);
useHotkeys(hotkeys[ReaderHotkey.NEXT_CHAPTER], () => openChapter('next'), [openChapter]); useHotkeys(hotkeys[ReaderHotkey.NEXT_CHAPTER], () => openChapter('next'), [openChapter]);

View File

@@ -7,7 +7,7 @@
*/ */
import { MutableRefObject, useCallback, useMemo } from 'react'; import { MutableRefObject, useCallback, useMemo } from 'react';
import { Direction } from '@mui/material/styles'; import { Direction, useTheme } from '@mui/material/styles';
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
import { getNextIndexFromPage, getNextPageIndex, getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; import { getNextIndexFromPage, getNextPageIndex, getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { getOptionForDirection } from '@/theme.tsx'; import { getOptionForDirection } from '@/theme.tsx';
@@ -21,7 +21,10 @@ import {
ReadingMode, ReadingMode,
} from '@/modules/reader/types/Reader.types.ts'; } from '@/modules/reader/types/Reader.types.ts';
import { ScrollDirection, ScrollOffset } from '@/modules/core/Core.types.ts'; import { ScrollDirection, ScrollOffset } from '@/modules/core/Core.types.ts';
import { ReaderScrollAmount } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; import {
ReaderScrollAmount,
READING_DIRECTION_TO_THEME_DIRECTION,
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { isEndOfPageInViewport, isPageInViewport } from '@/modules/reader/utils/ReaderPager.utils.tsx'; import { isEndOfPageInViewport, isPageInViewport } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx'; import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx';
import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx'; import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx';
@@ -33,20 +36,35 @@ import { Chapters } from '@/modules/chapter/services/Chapters.ts';
import { DirectionOffset } from '@/Base.types.ts'; import { DirectionOffset } from '@/Base.types.ts';
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
const READING_DIRECTION_TO_DIRECTION: Record<ReadingDirection, Direction> = { const getScrollDirectionInvert = (
[ReadingDirection.LTR]: 'ltr', scrollDirection: ScrollDirection,
[ReadingDirection.RTL]: 'rtl', scrollOffset: ScrollOffset,
}; themeDirection: Direction,
): 1 | -1 => {
if (scrollDirection === ScrollDirection.X) {
if (scrollOffset === ScrollOffset.BACKWARD) {
if (themeDirection === 'ltr') {
return -1;
}
const SCROLL_DIRECTION_BY_SCROLL_OFFSET_BY_READING_DIRECTION: Record<ReadingDirection, Record<ScrollOffset, 1 | -1>> = { return 1;
[ReadingDirection.LTR]: { }
[ScrollOffset.BACKWARD]: -1,
[ScrollOffset.FORWARD]: 1, if (scrollOffset === ScrollOffset.FORWARD) {
}, if (themeDirection === 'ltr') {
[ReadingDirection.RTL]: { return 1;
[ScrollOffset.BACKWARD]: 1, }
[ScrollOffset.FORWARD]: -1,
}, return -1;
}
}
// handle ScrollDirection.XY the same as ScrollDirection.Y
if (scrollOffset === ScrollOffset.BACKWARD) {
return -1;
}
return 1;
}; };
export class ReaderControls { export class ReaderControls {
@@ -55,7 +73,9 @@ export class ReaderControls {
static scroll( static scroll(
offset: ScrollOffset, offset: ScrollOffset,
direction: ScrollDirection, direction: ScrollDirection,
readingMode: ReadingMode,
readingDirection: ReadingDirection, readingDirection: ReadingDirection,
themeDirection: Direction,
element: HTMLElement, element: HTMLElement,
openChapter: ReturnType<(typeof ReaderControls)['useOpenChapter']>, openChapter: ReturnType<(typeof ReaderControls)['useOpenChapter']>,
scrollAmountPercentage: number = ReaderScrollAmount.LARGE, scrollAmountPercentage: number = ReaderScrollAmount.LARGE,
@@ -64,30 +84,36 @@ export class ReaderControls {
return; return;
} }
const themeDirectionOfReadingDirection = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
const areReadingDirectionsEqual = themeDirection === themeDirectionOfReadingDirection;
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
const isAtStartY = element.scrollTop === 0; const isAtStartY = element.scrollTop === 0;
const isAtEndY = const isAtEndY =
Math.floor(element.scrollTop) === element.scrollHeight - element.clientHeight || Math.floor(element.scrollTop) === element.scrollHeight - element.clientHeight ||
Math.ceil(element.scrollTop) === element.scrollHeight - element.clientHeight; Math.ceil(element.scrollTop) === element.scrollHeight - element.clientHeight;
const isAtStartX = element.scrollLeft === 0; const isAtStartX = element.scrollLeft === 0;
const isAtEndX = const isAtEndX =
Math.floor(element.scrollLeft) === element.scrollWidth - element.clientWidth || Math.floor(Math.abs(element.scrollLeft)) === element.scrollWidth - element.clientWidth ||
Math.ceil(element.scrollLeft) === element.scrollWidth - element.clientWidth; Math.ceil(Math.abs(element.scrollLeft)) === element.scrollWidth - element.clientWidth;
const isAtStartXForDirection = areReadingDirectionsEqual ? isAtStartX : isAtEndX;
const isAtEndXForDirection = areReadingDirectionsEqual ? isAtEndX : isAtStartX;
const scrollAmount = scrollAmountPercentage / 100; const scrollAmount = scrollAmountPercentage / 100;
const scrollDirection = SCROLL_DIRECTION_BY_SCROLL_OFFSET_BY_READING_DIRECTION[readingDirection][offset]; const scrollDirection = getScrollDirectionInvert(direction, offset, themeDirectionOfReadingDirection);
const getNewScrollPosition = (currentPos: number, elementSize: number) => const getNewScrollPosition = (currentPos: number, elementSize: number) =>
currentPos + elementSize * scrollAmount * scrollDirection; currentPos + elementSize * scrollAmount * scrollDirection;
switch (direction) { switch (direction) {
case ScrollDirection.X: case ScrollDirection.X:
if (isAtStartX && offset === ScrollOffset.BACKWARD) { if (isAtStartXForDirection && offset === ScrollOffset.BACKWARD && isContinuousReadingModeActive) {
openChapter('previous'); openChapter('previous', 'ltr');
return; return;
} }
if (isAtEndX && offset === ScrollOffset.FORWARD) { if (isAtEndXForDirection && offset === ScrollOffset.FORWARD && isContinuousReadingModeActive) {
openChapter('next'); openChapter('next', 'ltr');
return; return;
} }
@@ -97,13 +123,13 @@ export class ReaderControls {
}); });
break; break;
case ScrollDirection.Y: case ScrollDirection.Y:
if (isAtStartY && offset === ScrollOffset.BACKWARD) { if (isAtStartY && offset === ScrollOffset.BACKWARD && isContinuousReadingModeActive) {
openChapter('previous'); openChapter('previous', 'ltr');
return; return;
} }
if (isAtEndY && offset === ScrollOffset.FORWARD) { if (isAtEndY && offset === ScrollOffset.FORWARD && isContinuousReadingModeActive) {
openChapter('next'); openChapter('next', 'ltr');
return; return;
} }
@@ -117,22 +143,22 @@ export class ReaderControls {
} }
} }
static useOpenChapter(): (offset: 'previous' | 'next') => void { static useOpenChapter(): (offset: 'previous' | 'next', forceDirection?: Direction) => void {
const { readingDirection, readingMode } = ReaderService.useSettings(); const { readingDirection, readingMode } = ReaderService.useSettings();
const { previousChapter, nextChapter } = useReaderStateChaptersContext(); const { previousChapter, nextChapter } = useReaderStateChaptersContext();
const direction = READING_DIRECTION_TO_DIRECTION[readingDirection.value]; const direction = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection.value];
const openPreviousChapter = ReaderService.useNavigateToChapter(previousChapter, ReaderResumeMode.END); const openPreviousChapter = ReaderService.useNavigateToChapter(previousChapter, ReaderResumeMode.END);
const openNextChapter = ReaderService.useNavigateToChapter(nextChapter, ReaderResumeMode.START); const openNextChapter = ReaderService.useNavigateToChapter(nextChapter, ReaderResumeMode.START);
return useCallback( return useCallback(
(offset) => { (offset, forceDirection = direction) => {
switch (offset) { switch (offset) {
case 'previous': case 'previous':
getOptionForDirection(openPreviousChapter, openNextChapter, direction)(); getOptionForDirection(openPreviousChapter, openNextChapter, forceDirection)();
break; break;
case 'next': case 'next':
getOptionForDirection(openNextChapter, openPreviousChapter, direction)(); getOptionForDirection(openNextChapter, openPreviousChapter, forceDirection)();
break; break;
default: default:
throw new Error(`Unexpected "offset" (${offset})`); throw new Error(`Unexpected "offset" (${offset})`);
@@ -159,7 +185,7 @@ export class ReaderControls {
[currentPage, pages], [currentPage, pages],
); );
const indexOfLastPage = getNextIndexFromPage(pages[pages.length - 1]); const indexOfLastPage = getNextIndexFromPage(pages[pages.length - 1]);
const direction = READING_DIRECTION_TO_DIRECTION[readingDirection.value]; const direction = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection.value];
const isFirstPage = currentPageIndex === 0; const isFirstPage = currentPageIndex === 0;
const isLastPage = currentPageIndex === indexOfLastPage; const isLastPage = currentPageIndex === indexOfLastPage;
@@ -340,6 +366,7 @@ export class ReaderControls {
static useHandleClick( static useHandleClick(
scrollElement: HTMLElement | null, scrollElement: HTMLElement | null,
): (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void { ): (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void {
const { direction: themeDirection } = useTheme();
const { setIsVisible: setIsOverlayVisible } = useReaderOverlayContext(); const { setIsVisible: setIsOverlayVisible } = useReaderOverlayContext();
const { currentPageIndex, pages } = userReaderStatePagesContext(); const { currentPageIndex, pages } = userReaderStatePagesContext();
const { setShowPreview } = useReaderTapZoneContext(); const { setShowPreview } = useReaderTapZoneContext();
@@ -373,7 +400,9 @@ export class ReaderControls {
this.scroll( this.scroll(
ScrollOffset.BACKWARD, ScrollOffset.BACKWARD,
scrollDirection, scrollDirection,
readingMode.value,
readingDirection.value, readingDirection.value,
themeDirection,
scrollElement, scrollElement,
openChapter, openChapter,
); );
@@ -386,7 +415,9 @@ export class ReaderControls {
this.scroll( this.scroll(
ScrollOffset.FORWARD, ScrollOffset.FORWARD,
scrollDirection, scrollDirection,
readingMode.value,
readingDirection.value, readingDirection.value,
themeDirection,
scrollElement, scrollElement,
openChapter, openChapter,
); );
@@ -398,7 +429,16 @@ export class ReaderControls {
throw new Error(`Unexpected "TapZoneRegionType" (${action})`); throw new Error(`Unexpected "TapZoneRegionType" (${action})`);
} }
}, },
[scrollElement, currentPageIndex, pages, readingMode.value, openPage, readingDirection.value, openChapter], [
scrollElement,
currentPageIndex,
pages,
readingMode.value,
openPage,
readingDirection.value,
openChapter,
themeDirection,
],
); );
} }
} }