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:
@@ -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]);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user