Add "automatic scrolling"
This commit is contained in:
@@ -844,6 +844,11 @@
|
|||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"active_setting": "Active setting",
|
"active_setting": "Active setting",
|
||||||
|
"auto_scroll": {
|
||||||
|
"smooth": "Smooth auto scrolling",
|
||||||
|
"speed": "Auto scroll speed",
|
||||||
|
"title": "Auto scroll"
|
||||||
|
},
|
||||||
"auto_webtoon_mode": {
|
"auto_webtoon_mode": {
|
||||||
"description": "Automatically use webtoon mode for entries that are detected to likely use the long strip format",
|
"description": "Automatically use webtoon mode for entries that are detected to likely use the long strip format",
|
||||||
"title": "Auto webtoon mode"
|
"title": "Auto webtoon mode"
|
||||||
@@ -940,6 +945,7 @@
|
|||||||
"title": "Tap zones"
|
"title": "Tap zones"
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
"quick_settings": "Quick settings",
|
||||||
"reader_settings": "Reader Settings"
|
"reader_settings": "Reader Settings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
160
src/modules/core/hooks/useAutomaticScrolling.ts
Normal file
160
src/modules/core/hooks/useAutomaticScrolling.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* 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 { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { ScrollDirection } from '@/modules/core/Core.types.ts';
|
||||||
|
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||||
|
|
||||||
|
// in case the scroll amount is not enough "scrollBy" just does not do anything
|
||||||
|
const MIN_SCROLL_AMOUNT_PX = 1.5;
|
||||||
|
const getScrollAmount = (amountPerMs: number, speedMs: number): { amountPx: number; speedMs: number } => {
|
||||||
|
const amountPx = amountPerMs * speedMs;
|
||||||
|
|
||||||
|
if (amountPx >= MIN_SCROLL_AMOUNT_PX) {
|
||||||
|
return { amountPx, speedMs };
|
||||||
|
}
|
||||||
|
|
||||||
|
return getScrollAmount(amountPerMs, speedMs + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPxPerMs(size: number, scrollAmountPercentage: number, scrollSpeedMs: number) {
|
||||||
|
return (size * (scrollAmountPercentage / 100)) / scrollSpeedMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAutomaticScrolling = (
|
||||||
|
refOrCallback: MutableRefObject<HTMLElement | null> | (() => void) | undefined,
|
||||||
|
scrollPerSecond: number,
|
||||||
|
scrollDirection: Exclude<ScrollDirection, ScrollDirection.XY> = ScrollDirection.Y,
|
||||||
|
scrollAmountPercentage: number = 100,
|
||||||
|
invert: boolean = false,
|
||||||
|
smooth: boolean = false,
|
||||||
|
): {
|
||||||
|
isActive: boolean;
|
||||||
|
isPaused: boolean;
|
||||||
|
start: () => void;
|
||||||
|
cancel: () => void;
|
||||||
|
toggleActive: () => void;
|
||||||
|
pause: () => void;
|
||||||
|
resume: () => void;
|
||||||
|
} => {
|
||||||
|
const isCallback = typeof refOrCallback === 'function';
|
||||||
|
|
||||||
|
const elementStyle = useRef<CSSStyleDeclaration>();
|
||||||
|
const scrollTriggerTimer = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
|
|
||||||
|
// scroll amount is based on the screen dimensions, thus, the hook needs to update in case they change
|
||||||
|
const [screenDimensions, setScreenDimensions] = useState({ width: window.innerWidth, height: window.innerHeight });
|
||||||
|
useResizeObserver(
|
||||||
|
document.documentElement,
|
||||||
|
useCallback(() => {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
if (screenDimensions.width !== width || screenDimensions.height !== height) {
|
||||||
|
setScreenDimensions({ width, height });
|
||||||
|
}
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
const start = useCallback(() => {
|
||||||
|
setIsActive(true);
|
||||||
|
}, []);
|
||||||
|
const cancel = useCallback(() => {
|
||||||
|
setIsActive(false);
|
||||||
|
setIsPaused(false);
|
||||||
|
clearInterval(scrollTriggerTimer.current);
|
||||||
|
}, []);
|
||||||
|
const toggleActive = useCallback(() => {
|
||||||
|
if (isActive) {
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
|
}, [isActive]);
|
||||||
|
const pause = useCallback(() => {
|
||||||
|
setIsPaused(true);
|
||||||
|
}, []);
|
||||||
|
const resume = useCallback(() => {
|
||||||
|
setIsPaused(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!refOrCallback) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollSpeedMs = scrollPerSecond * 1000;
|
||||||
|
|
||||||
|
const startScrolling = (callback: () => void, timeout: number) => {
|
||||||
|
clearInterval(scrollTriggerTimer.current);
|
||||||
|
scrollTriggerTimer.current = setInterval(() => {
|
||||||
|
if (isActive && !isPaused) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isCallback) {
|
||||||
|
startScrolling(refOrCallback, scrollSpeedMs);
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = refOrCallback.current;
|
||||||
|
if (!element) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!elementStyle.current) {
|
||||||
|
elementStyle.current = getComputedStyle(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRTL = elementStyle.current.direction;
|
||||||
|
const handleScrollX = scrollDirection !== ScrollDirection.Y;
|
||||||
|
const handleScrollY = scrollDirection !== ScrollDirection.X;
|
||||||
|
|
||||||
|
const pxPerMsX = getPxPerMs(window.innerWidth, scrollAmountPercentage, scrollSpeedMs);
|
||||||
|
const pxPerMsY = getPxPerMs(window.innerHeight, scrollAmountPercentage, scrollSpeedMs);
|
||||||
|
|
||||||
|
const tmpScrollSpeedMs = smooth ? 1 : scrollSpeedMs;
|
||||||
|
const { amountPx: pxScrollAmountX, speedMs: timeoutMsX } = getScrollAmount(pxPerMsX, tmpScrollSpeedMs);
|
||||||
|
const { amountPx: pxScrollAmountY, speedMs: timeoutMsY } = getScrollAmount(pxPerMsY, tmpScrollSpeedMs);
|
||||||
|
|
||||||
|
const timeoutMs = handleScrollX ? timeoutMsX : timeoutMsY;
|
||||||
|
|
||||||
|
const scrollTopByBase = handleScrollY ? pxScrollAmountX : 0;
|
||||||
|
const scrollLeftByBase = handleScrollX ? pxScrollAmountY : 0;
|
||||||
|
const scrollTopByReadingMode = scrollTopByBase;
|
||||||
|
const scrollLeftByReadingMode = isRTL ? -Math.abs(scrollLeftByBase) : scrollLeftByBase;
|
||||||
|
const scrollTopByInverted = invert ? -Math.abs(scrollTopByReadingMode) : scrollTopByReadingMode;
|
||||||
|
const scrollLeftByInverted = invert ? -Math.abs(scrollLeftByReadingMode) : scrollLeftByReadingMode;
|
||||||
|
|
||||||
|
startScrolling(() => {
|
||||||
|
element.scrollBy({
|
||||||
|
top: scrollTopByInverted,
|
||||||
|
left: scrollLeftByInverted,
|
||||||
|
// arg "smooth" triggers the interval so fast that using "behavior smooth" doesn't look smooth and also slows down the scrolling
|
||||||
|
behavior: smooth ? undefined : 'smooth',
|
||||||
|
});
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
return () => clearInterval(scrollTriggerTimer.current);
|
||||||
|
}, [refOrCallback, scrollPerSecond, scrollAmountPercentage, screenDimensions, isActive, isPaused]);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({ isActive, isPaused, start, cancel, toggleActive, pause, resume }),
|
||||||
|
[isActive, isPaused, start, cancel, toggleActive, pause, resume],
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -69,6 +69,7 @@ const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
|||||||
hotkeys: undefined,
|
hotkeys: undefined,
|
||||||
imagePreLoadAmount: undefined,
|
imagePreLoadAmount: undefined,
|
||||||
shouldUseAutoWebtoonMode: undefined,
|
shouldUseAutoWebtoonMode: undefined,
|
||||||
|
autoScroll: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
|
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import { useEffect } from 'react';
|
|||||||
import { useTheme } from '@mui/material/styles';
|
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 { IReaderSettings, ReaderHotkey, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
|
import { IReaderSettings, ReaderHotkey } from '@/modules/reader/types/Reader.types.ts';
|
||||||
import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx';
|
import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx';
|
||||||
import { getNextRotationValue } from '@/modules/core/utils/ValueRotationButton.utils.ts';
|
import { getNextRotationValue } from '@/modules/core/utils/ValueRotationButton.utils.ts';
|
||||||
import {
|
import {
|
||||||
|
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION,
|
||||||
READER_PAGE_SCALE_MODE_VALUES,
|
READER_PAGE_SCALE_MODE_VALUES,
|
||||||
ReaderScrollAmount,
|
ReaderScrollAmount,
|
||||||
READING_DIRECTION_VALUES,
|
READING_DIRECTION_VALUES,
|
||||||
@@ -23,9 +24,10 @@ import {
|
|||||||
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||||
import { HotkeyScope } from '@/modules/hotkeys/Hotkeys.types.ts';
|
import { HotkeyScope } from '@/modules/hotkeys/Hotkeys.types.ts';
|
||||||
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
|
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
|
||||||
import { ScrollDirection, ScrollOffset } from '@/modules/core/Core.types.ts';
|
import { ScrollOffset } from '@/modules/core/Core.types.ts';
|
||||||
import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts';
|
import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts';
|
||||||
import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts';
|
import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts';
|
||||||
|
import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||||
|
|
||||||
const useHotkeys = (...args: Parameters<typeof useHotKeysHook>): ReturnType<typeof useHotKeysHook> => {
|
const useHotkeys = (...args: Parameters<typeof useHotKeysHook>): ReturnType<typeof useHotKeysHook> => {
|
||||||
const [keys, callback, options, dependencies] = args;
|
const [keys, callback, options, dependencies] = args;
|
||||||
@@ -57,14 +59,6 @@ const updateSettingCycleThrough = <Setting extends keyof IReaderSettings>(
|
|||||||
updateSetting(setting, nextValue);
|
updateSetting(setting, nextValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION: Record<ReadingMode, ScrollDirection> = {
|
|
||||||
[ReadingMode.SINGLE_PAGE]: ScrollDirection.Y,
|
|
||||||
[ReadingMode.DOUBLE_PAGE]: ScrollDirection.Y,
|
|
||||||
[ReadingMode.CONTINUOUS_VERTICAL]: ScrollDirection.Y,
|
|
||||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: ScrollDirection.X,
|
|
||||||
[ReadingMode.WEBTOON]: ScrollDirection.Y,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ReaderHotkeys = ({
|
export const ReaderHotkeys = ({
|
||||||
scrollElementRef,
|
scrollElementRef,
|
||||||
}: {
|
}: {
|
||||||
@@ -77,6 +71,7 @@ export const ReaderHotkeys = ({
|
|||||||
const { isVisible, setIsVisible: setIsOverlayVisible } = useReaderOverlayContext();
|
const { isVisible, setIsVisible: setIsOverlayVisible } = useReaderOverlayContext();
|
||||||
const { hotkeys, pageScaleMode, shouldStretchPage, shouldOffsetDoubleSpreads, readingMode, readingDirection } =
|
const { hotkeys, pageScaleMode, shouldStretchPage, shouldOffsetDoubleSpreads, readingMode, readingDirection } =
|
||||||
ReaderService.useSettings();
|
ReaderService.useSettings();
|
||||||
|
const automaticScrolling = useReaderAutoScrollContext();
|
||||||
|
|
||||||
const openChapter = ReaderControls.useOpenChapter();
|
const openChapter = ReaderControls.useOpenChapter();
|
||||||
const openPage = ReaderControls.useOpenPage();
|
const openPage = ReaderControls.useOpenPage();
|
||||||
@@ -188,6 +183,10 @@ export const ReaderHotkeys = ({
|
|||||||
},
|
},
|
||||||
[updateSetting, deleteSetting, readingDirection.value, readingDirection.isDefault],
|
[updateSetting, deleteSetting, readingDirection.value, readingDirection.isDefault],
|
||||||
);
|
);
|
||||||
|
useHotkeys(hotkeys[ReaderHotkey.TOGGLE_AUTO_SCROLL], automaticScrolling.toggleActive, { preventDefault: true }, [
|
||||||
|
updateSetting,
|
||||||
|
automaticScrolling.toggleActive,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
enableScope(HotkeyScope.READER);
|
enableScope(HotkeyScope.READER);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useTranslation } from 'react-i18next';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import PauseCircleFilledIcon from '@mui/icons-material/PauseCircleFilled';
|
||||||
|
import PlayCircleFilledIcon from '@mui/icons-material/PlayCircleFilled';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||||
|
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
|
||||||
|
import { AUTO_SCROLL_SPEED } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||||
|
|
||||||
|
export const ReaderNavBarDesktopAutoScroll = ({
|
||||||
|
autoScroll,
|
||||||
|
setAutoScroll,
|
||||||
|
}: Pick<IReaderSettings, 'autoScroll'> & {
|
||||||
|
setAutoScroll: (newAutoScroll: IReaderSettings['autoScroll'], commit: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isActive, toggleActive } = useReaderAutoScrollContext();
|
||||||
|
|
||||||
|
const updateTimeout = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack sx={{ flexDirection: 'row', gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
sx={{ justifyContent: 'start', textTransform: 'unset', flexGrow: 1 }}
|
||||||
|
size="large"
|
||||||
|
onClick={toggleActive}
|
||||||
|
color={isActive ? 'secondary' : 'primary'}
|
||||||
|
variant="contained"
|
||||||
|
startIcon={isActive ? <PauseCircleFilledIcon /> : <PlayCircleFilledIcon />}
|
||||||
|
>
|
||||||
|
{t('reader.settings.auto_scroll.title')}
|
||||||
|
</Button>
|
||||||
|
<TextField
|
||||||
|
value={autoScroll.value}
|
||||||
|
type="number"
|
||||||
|
onBlur={(e) => {
|
||||||
|
const value = +e.target.value;
|
||||||
|
|
||||||
|
if (value !== autoScroll.value) {
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
setAutoScroll({ ...autoScroll, value }, true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = +e.target.value;
|
||||||
|
setAutoScroll({ ...autoScroll, value }, false);
|
||||||
|
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
updateTimeout.current = setTimeout(() => setAutoScroll({ ...autoScroll, value }, true), 1000);
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
inputProps: AUTO_SCROLL_SPEED,
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
{t('global.time.seconds.second', { count: autoScroll.value })}
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
|||||||
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
||||||
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||||
import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts';
|
import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts';
|
||||||
|
import { ReaderNavBarDesktopAutoScroll } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopAutoScroll.tsx';
|
||||||
|
|
||||||
const BaseReaderNavBarDesktopQuickSettings = ({
|
const BaseReaderNavBarDesktopQuickSettings = ({
|
||||||
manga,
|
manga,
|
||||||
@@ -28,12 +29,18 @@ const BaseReaderNavBarDesktopQuickSettings = ({
|
|||||||
pageScaleMode,
|
pageScaleMode,
|
||||||
shouldStretchPage,
|
shouldStretchPage,
|
||||||
readingDirection,
|
readingDirection,
|
||||||
|
autoScroll,
|
||||||
openSettings,
|
openSettings,
|
||||||
}: Pick<TReaderStateMangaContext, 'manga'> &
|
}: Pick<TReaderStateMangaContext, 'manga'> &
|
||||||
Pick<ReaderNavBarDesktopProps, 'openSettings'> &
|
Pick<ReaderNavBarDesktopProps, 'openSettings'> &
|
||||||
Pick<
|
Pick<
|
||||||
IReaderSettingsWithDefaultFlag,
|
IReaderSettingsWithDefaultFlag,
|
||||||
'readingMode' | 'shouldOffsetDoubleSpreads' | 'pageScaleMode' | 'shouldStretchPage' | 'readingDirection'
|
| 'readingMode'
|
||||||
|
| 'shouldOffsetDoubleSpreads'
|
||||||
|
| 'pageScaleMode'
|
||||||
|
| 'shouldStretchPage'
|
||||||
|
| 'readingDirection'
|
||||||
|
| 'autoScroll'
|
||||||
>) => {
|
>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -66,6 +73,10 @@ const BaseReaderNavBarDesktopQuickSettings = ({
|
|||||||
isDefaultable
|
isDefaultable
|
||||||
onDefault={() => deleteSetting('readingDirection')}
|
onDefault={() => deleteSetting('readingDirection')}
|
||||||
/>
|
/>
|
||||||
|
<ReaderNavBarDesktopAutoScroll
|
||||||
|
autoScroll={autoScroll}
|
||||||
|
setAutoScroll={(...args) => updateSetting('autoScroll', ...args)}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => openSettings()}
|
onClick={() => openSettings()}
|
||||||
size="large"
|
size="large"
|
||||||
@@ -82,5 +93,13 @@ const BaseReaderNavBarDesktopQuickSettings = ({
|
|||||||
export const ReaderNavBarDesktopQuickSettings = withPropsFrom(
|
export const ReaderNavBarDesktopQuickSettings = withPropsFrom(
|
||||||
BaseReaderNavBarDesktopQuickSettings,
|
BaseReaderNavBarDesktopQuickSettings,
|
||||||
[useReaderStateMangaContext, ReaderService.useSettings],
|
[useReaderStateMangaContext, ReaderService.useSettings],
|
||||||
['manga', 'readingMode', 'shouldOffsetDoubleSpreads', 'pageScaleMode', 'shouldStretchPage', 'readingDirection'],
|
[
|
||||||
|
'manga',
|
||||||
|
'readingMode',
|
||||||
|
'shouldOffsetDoubleSpreads',
|
||||||
|
'pageScaleMode',
|
||||||
|
'shouldStretchPage',
|
||||||
|
'readingDirection',
|
||||||
|
'autoScroll',
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const BaseReaderBottomBarMobile = ({
|
|||||||
<FormatListBulletedIcon />
|
<FormatListBulletedIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('reader.settings.label.reader_type')}>
|
<Tooltip title={t('reader.settings.title.quick_settings')}>
|
||||||
<IconButton {...bindTrigger(quickSettingsPopupState)} size="large" color="inherit">
|
<IconButton {...bindTrigger(quickSettingsPopupState)} size="large" color="inherit">
|
||||||
<AppSettingsAltIcon />
|
<AppSettingsAltIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -8,21 +8,34 @@
|
|||||||
|
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReaderSettingReadingMode } from '@/modules/reader/components/settings/layout/ReaderSettingReadingMode.tsx';
|
import { ReaderSettingReadingMode } from '@/modules/reader/components/settings/layout/ReaderSettingReadingMode.tsx';
|
||||||
import { ReaderSettingReadingDirection } from '@/modules/reader/components/settings/layout/ReaderSettingReadingDirection.tsx';
|
import { ReaderSettingReadingDirection } from '@/modules/reader/components/settings/layout/ReaderSettingReadingDirection.tsx';
|
||||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||||
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||||
import { DefaultSettingFootnote } from '@/modules/reader/components/settings/DefaultSettingFootnote.tsx';
|
import { DefaultSettingFootnote } from '@/modules/reader/components/settings/DefaultSettingFootnote.tsx';
|
||||||
import { IReaderSettingsWithDefaultFlag, TReaderStateMangaContext } from '@/modules/reader/types/Reader.types.ts';
|
import {
|
||||||
|
IReaderSettingsWithDefaultFlag,
|
||||||
|
TReaderAutoScrollContext,
|
||||||
|
TReaderStateMangaContext,
|
||||||
|
} from '@/modules/reader/types/Reader.types.ts';
|
||||||
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
||||||
import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts';
|
import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts';
|
||||||
|
import { ReaderSettingAutoScroll } from '@/modules/reader/components/settings/behaviour/ReaderSettingAutoScroll.tsx';
|
||||||
|
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||||
|
import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||||
|
|
||||||
const BaseReaderBottomBarMobileQuickSettings = ({
|
const BaseReaderBottomBarMobileQuickSettings = ({
|
||||||
manga,
|
manga,
|
||||||
readingMode,
|
readingMode,
|
||||||
readingDirection,
|
readingDirection,
|
||||||
|
autoScroll,
|
||||||
|
isActive,
|
||||||
|
toggleActive,
|
||||||
}: Pick<TReaderStateMangaContext, 'manga'> &
|
}: Pick<TReaderStateMangaContext, 'manga'> &
|
||||||
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'readingDirection'>) => {
|
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'readingDirection' | 'autoScroll'> &
|
||||||
|
Pick<TReaderAutoScrollContext, 'isActive' | 'toggleActive'>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? FALLBACK_MANGA);
|
const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? FALLBACK_MANGA);
|
||||||
|
|
||||||
if (!manga) {
|
if (!manga) {
|
||||||
@@ -44,12 +57,21 @@ const BaseReaderBottomBarMobileQuickSettings = ({
|
|||||||
isDefaultable
|
isDefaultable
|
||||||
onDefault={() => deleteSetting('readingDirection')}
|
onDefault={() => deleteSetting('readingDirection')}
|
||||||
/>
|
/>
|
||||||
|
<CheckboxInput
|
||||||
|
label={t('reader.settings.auto_scroll.title')}
|
||||||
|
checked={isActive}
|
||||||
|
onChange={() => toggleActive()}
|
||||||
|
/>
|
||||||
|
<ReaderSettingAutoScroll
|
||||||
|
autoScroll={autoScroll}
|
||||||
|
setAutoScroll={(...args) => ReaderService.updateSetting(manga, 'autoScroll', ...args)}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReaderBottomBarMobileQuickSettings = withPropsFrom(
|
export const ReaderBottomBarMobileQuickSettings = withPropsFrom(
|
||||||
memo(BaseReaderBottomBarMobileQuickSettings),
|
memo(BaseReaderBottomBarMobileQuickSettings),
|
||||||
[useReaderStateMangaContext, ReaderService.useSettings],
|
[useReaderStateMangaContext, ReaderService.useSettings, useReaderAutoScrollContext],
|
||||||
['manga', 'readingMode', 'readingDirection'],
|
['manga', 'readingMode', 'readingDirection', 'autoScroll', 'isActive', 'toggleActive'],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ReaderSettingExitMode } from '@/modules/reader/components/settings/beha
|
|||||||
import { isOffsetDoubleSpreadPagesEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
import { isOffsetDoubleSpreadPagesEditable } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||||
import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx';
|
import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx';
|
||||||
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||||
|
import { ReaderSettingAutoScroll } from '@/modules/reader/components/settings/behaviour/ReaderSettingAutoScroll.tsx';
|
||||||
|
|
||||||
export const ReaderBehaviourSettings = ({
|
export const ReaderBehaviourSettings = ({
|
||||||
settings,
|
settings,
|
||||||
@@ -60,6 +61,10 @@ export const ReaderBehaviourSettings = ({
|
|||||||
checked={settings.shouldUseAutoWebtoonMode}
|
checked={settings.shouldUseAutoWebtoonMode}
|
||||||
onChange={(_, checked) => updateSetting('shouldUseAutoWebtoonMode', checked)}
|
onChange={(_, checked) => updateSetting('shouldUseAutoWebtoonMode', checked)}
|
||||||
/>
|
/>
|
||||||
|
<ReaderSettingAutoScroll
|
||||||
|
autoScroll={settings.autoScroll}
|
||||||
|
setAutoScroll={(...args) => updateSetting('autoScroll', ...args)}
|
||||||
|
/>
|
||||||
<SliderInput
|
<SliderInput
|
||||||
label={t('reader.settings.image_preload_amount')}
|
label={t('reader.settings.image_preload_amount')}
|
||||||
value={settings.imagePreLoadAmount}
|
value={settings.imagePreLoadAmount}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useTranslation } from 'react-i18next';
|
||||||
|
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||||
|
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
|
||||||
|
import { SliderInput } from '@/modules/core/components/inputs/SliderInput.tsx';
|
||||||
|
import { AUTO_SCROLL_SPEED, DEFAULT_READER_SETTINGS } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||||
|
|
||||||
|
export const ReaderSettingAutoScroll = ({
|
||||||
|
autoScroll,
|
||||||
|
setAutoScroll,
|
||||||
|
}: Pick<IReaderSettings, 'autoScroll'> & {
|
||||||
|
setAutoScroll: (updatedAutoScroll: IReaderSettings['autoScroll'], commit: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CheckboxInput
|
||||||
|
label={t('reader.settings.auto_scroll.smooth')}
|
||||||
|
checked={autoScroll.smooth}
|
||||||
|
onChange={(_, checked) => setAutoScroll({ ...autoScroll, smooth: checked }, true)}
|
||||||
|
/>
|
||||||
|
<SliderInput
|
||||||
|
label={t('reader.settings.auto_scroll.speed')}
|
||||||
|
value={t('global.time.seconds.value', { count: autoScroll.value })}
|
||||||
|
onDefault={() =>
|
||||||
|
setAutoScroll({ ...autoScroll, value: DEFAULT_READER_SETTINGS.autoScroll.value }, true)
|
||||||
|
}
|
||||||
|
slotProps={{
|
||||||
|
slider: {
|
||||||
|
defaultValue: DEFAULT_READER_SETTINGS.customFilter.saturate.value,
|
||||||
|
value: autoScroll.value,
|
||||||
|
...AUTO_SCROLL_SPEED,
|
||||||
|
onChange: (_, value) => {
|
||||||
|
setAutoScroll({ ...autoScroll, value: value as number }, false);
|
||||||
|
},
|
||||||
|
onChangeCommitted: (_, value) => {
|
||||||
|
setAutoScroll({ ...autoScroll, value: value as number }, true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -55,6 +55,7 @@ import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
|
|||||||
import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.types.ts';
|
import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.types.ts';
|
||||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||||
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
||||||
|
import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||||
|
|
||||||
const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType> = {
|
const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType> = {
|
||||||
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
|
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
|
||||||
@@ -114,6 +115,9 @@ const BaseReaderViewer = forwardRef(
|
|||||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||||
const isDragging = useMouseDragScroll(isContinuousReadingModeActive ? scrollElementRef : undefined);
|
const isDragging = useMouseDragScroll(isContinuousReadingModeActive ? scrollElementRef : undefined);
|
||||||
|
|
||||||
|
const automaticScrolling = useReaderAutoScrollContext();
|
||||||
|
useEffect(() => automaticScrolling.setScrollRef(scrollElementRef), []);
|
||||||
|
|
||||||
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
|
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
|
||||||
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
|
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -320,6 +324,16 @@ const BaseReaderViewer = forwardRef(
|
|||||||
};
|
};
|
||||||
}, [isOverlayVisible]);
|
}, [isOverlayVisible]);
|
||||||
|
|
||||||
|
// handle auto scrolling
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOverlayVisible) {
|
||||||
|
automaticScrolling.pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
automaticScrolling.resume();
|
||||||
|
}, [isOverlayVisible, automaticScrolling.isPaused]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
ref={scrollElementRef}
|
ref={scrollElementRef}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import ExpandIcon from '@mui/icons-material/Expand';
|
|||||||
import CropOriginalIcon from '@mui/icons-material/CropOriginal';
|
import CropOriginalIcon from '@mui/icons-material/CropOriginal';
|
||||||
import { TooltipProps } from '@mui/material/Tooltip';
|
import { TooltipProps } from '@mui/material/Tooltip';
|
||||||
import { Direction } from '@mui/material/styles';
|
import { Direction } from '@mui/material/styles';
|
||||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
import { ScrollDirection, ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||||
import {
|
import {
|
||||||
IReaderSettings,
|
IReaderSettings,
|
||||||
IReaderSettingsGlobal,
|
IReaderSettingsGlobal,
|
||||||
@@ -56,6 +56,7 @@ const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefine
|
|||||||
hotkeys: undefined,
|
hotkeys: undefined,
|
||||||
imagePreLoadAmount: undefined,
|
imagePreLoadAmount: undefined,
|
||||||
shouldUseAutoWebtoonMode: undefined,
|
shouldUseAutoWebtoonMode: undefined,
|
||||||
|
autoScroll: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT);
|
export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT);
|
||||||
@@ -122,9 +123,14 @@ export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
|||||||
[ReaderHotkey.OFFSET_SPREAD_PAGES]: ['o'],
|
[ReaderHotkey.OFFSET_SPREAD_PAGES]: ['o'],
|
||||||
[ReaderHotkey.CYCLE_READING_MODE]: ['r'],
|
[ReaderHotkey.CYCLE_READING_MODE]: ['r'],
|
||||||
[ReaderHotkey.CYCLE_READING_DIRECTION]: ['t'],
|
[ReaderHotkey.CYCLE_READING_DIRECTION]: ['t'],
|
||||||
|
[ReaderHotkey.TOGGLE_AUTO_SCROLL]: ['space'],
|
||||||
},
|
},
|
||||||
imagePreLoadAmount: 5,
|
imagePreLoadAmount: 5,
|
||||||
shouldUseAutoWebtoonMode: true,
|
shouldUseAutoWebtoonMode: true,
|
||||||
|
autoScroll: {
|
||||||
|
value: 5,
|
||||||
|
smooth: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {
|
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {
|
||||||
@@ -249,6 +255,7 @@ export const READER_SETTING_TABS: Record<
|
|||||||
*/
|
*/
|
||||||
export enum ReaderScrollAmount {
|
export enum ReaderScrollAmount {
|
||||||
SMALL = 25,
|
SMALL = 25,
|
||||||
|
MEDIUM = 75,
|
||||||
LARGE = 95,
|
LARGE = 95,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,3 +269,20 @@ export const READER_BACKGROUND_TO_COLOR = {
|
|||||||
[ReaderBackgroundColor.GRAY]: 'grey.200',
|
[ReaderBackgroundColor.GRAY]: 'grey.200',
|
||||||
[ReaderBackgroundColor.WHITE]: 'common.white',
|
[ReaderBackgroundColor.WHITE]: 'common.white',
|
||||||
} as const satisfies Record<ReaderBackgroundColor, string>;
|
} as const satisfies Record<ReaderBackgroundColor, string>;
|
||||||
|
|
||||||
|
export const CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION: Record<
|
||||||
|
ReadingMode,
|
||||||
|
Exclude<ScrollDirection, ScrollDirection.XY>
|
||||||
|
> = {
|
||||||
|
[ReadingMode.SINGLE_PAGE]: ScrollDirection.Y,
|
||||||
|
[ReadingMode.DOUBLE_PAGE]: ScrollDirection.Y,
|
||||||
|
[ReadingMode.CONTINUOUS_VERTICAL]: ScrollDirection.Y,
|
||||||
|
[ReadingMode.CONTINUOUS_HORIZONTAL]: ScrollDirection.X,
|
||||||
|
[ReadingMode.WEBTOON]: ScrollDirection.Y,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AUTO_SCROLL_SPEED = {
|
||||||
|
MIN: 0.5,
|
||||||
|
MAX: 60,
|
||||||
|
STEP: 0.5,
|
||||||
|
};
|
||||||
|
|||||||
19
src/modules/reader/contexts/ReaderAutoScrollContext.tsx
Normal file
19
src/modules/reader/contexts/ReaderAutoScrollContext.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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 { createContext, useContext } from 'react';
|
||||||
|
import { TReaderAutoScrollContext } from '@/modules/reader/types/Reader.types.ts';
|
||||||
|
|
||||||
|
export const ReaderAutoScrollContext = createContext<TReaderAutoScrollContext>({
|
||||||
|
isActive: false,
|
||||||
|
setScrollRef: () => {},
|
||||||
|
start: () => {},
|
||||||
|
cancel: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useReaderAutoScrollContext = () => useContext(ReaderAutoScrollContext);
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* 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 { memo, ReactNode, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { Direction, useTheme } from '@mui/material/styles';
|
||||||
|
import { useAutomaticScrolling } from '@/modules/core/hooks/useAutomaticScrolling.ts';
|
||||||
|
import { IReaderSettings, TReaderAutoScrollContext } from '@/modules/reader/types/Reader.types.ts';
|
||||||
|
import { ReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||||
|
import { ReaderControls } from '@/modules/reader/services/ReaderControls';
|
||||||
|
import { isContinuousReadingMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||||
|
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
||||||
|
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||||
|
import {
|
||||||
|
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION,
|
||||||
|
ReaderScrollAmount,
|
||||||
|
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
|
||||||
|
|
||||||
|
const BaseReaderAutoScrollContextProvider = ({
|
||||||
|
children,
|
||||||
|
openPage,
|
||||||
|
readingMode,
|
||||||
|
autoScroll,
|
||||||
|
themeDirection,
|
||||||
|
combinedDirection,
|
||||||
|
}: Pick<IReaderSettings, 'readingMode' | 'autoScroll'> & {
|
||||||
|
children: ReactNode;
|
||||||
|
openPage: ReturnType<typeof ReaderControls.useOpenPage>;
|
||||||
|
themeDirection: Direction;
|
||||||
|
combinedDirection: Direction;
|
||||||
|
}) => {
|
||||||
|
const [scrollRef, setScrollRef] = useState<TReaderAutoScrollContext['scrollRef']>();
|
||||||
|
|
||||||
|
const invertScrolling = themeDirection !== combinedDirection;
|
||||||
|
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||||
|
|
||||||
|
const changePage = useCallback(() => {
|
||||||
|
openPage('next');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const automaticScrolling = useAutomaticScrolling(
|
||||||
|
isContinuousReadingModeActive ? scrollRef : changePage,
|
||||||
|
autoScroll.value,
|
||||||
|
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode],
|
||||||
|
ReaderScrollAmount.MEDIUM,
|
||||||
|
invertScrolling,
|
||||||
|
autoScroll.smooth,
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
scrollRef,
|
||||||
|
setScrollRef,
|
||||||
|
...automaticScrolling,
|
||||||
|
}),
|
||||||
|
[scrollRef, automaticScrolling],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <ReaderAutoScrollContext.Provider value={value}>{children}</ReaderAutoScrollContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReaderAutoScrollContextProvider = withPropsFrom(
|
||||||
|
memo(BaseReaderAutoScrollContextProvider),
|
||||||
|
[
|
||||||
|
ReaderService.useSettingsWithoutDefaultFlag,
|
||||||
|
() => ({ openPage: ReaderControls.useOpenPage() }),
|
||||||
|
() => ({ themeDirection: useTheme().direction }),
|
||||||
|
() => ({ combinedDirection: ReaderService.useGetThemeDirection() }),
|
||||||
|
],
|
||||||
|
['openPage', 'readingMode', 'themeDirection', 'combinedDirection', 'autoScroll'],
|
||||||
|
);
|
||||||
@@ -12,13 +12,16 @@ import { ReaderProgressBarContextProvider } from '@/modules/reader/contexts/Read
|
|||||||
import { ReaderOverlayContextProvider } from '@/modules/reader/contexts/ReaderOverlayContextProvider.tsx';
|
import { ReaderOverlayContextProvider } from '@/modules/reader/contexts/ReaderOverlayContextProvider.tsx';
|
||||||
import { ReaderStateContextProvider } from '@/modules/reader/contexts/state/ReaderStateContextProvider.tsx';
|
import { ReaderStateContextProvider } from '@/modules/reader/contexts/state/ReaderStateContextProvider.tsx';
|
||||||
import { ReaderScrollbarContextProvider } from '@/modules/reader/contexts/ReaderScrollbarContextProvider.tsx';
|
import { ReaderScrollbarContextProvider } from '@/modules/reader/contexts/ReaderScrollbarContextProvider.tsx';
|
||||||
|
import { ReaderAutoScrollContextProvider } from '@/modules/reader/contexts/ReaderAutoScrollContextProvider.tsx';
|
||||||
|
|
||||||
export const ReaderContextProvider = ({ children }: { children?: ReactNode }) => (
|
export const ReaderContextProvider = ({ children }: { children?: ReactNode }) => (
|
||||||
<ReaderStateContextProvider>
|
<ReaderStateContextProvider>
|
||||||
<ReaderTapZoneContextProvider>
|
<ReaderTapZoneContextProvider>
|
||||||
<ReaderOverlayContextProvider>
|
<ReaderOverlayContextProvider>
|
||||||
<ReaderProgressBarContextProvider>
|
<ReaderProgressBarContextProvider>
|
||||||
<ReaderScrollbarContextProvider>{children}</ReaderScrollbarContextProvider>
|
<ReaderScrollbarContextProvider>
|
||||||
|
<ReaderAutoScrollContextProvider>{children}</ReaderAutoScrollContextProvider>
|
||||||
|
</ReaderScrollbarContextProvider>
|
||||||
</ReaderProgressBarContextProvider>
|
</ReaderProgressBarContextProvider>
|
||||||
</ReaderOverlayContextProvider>
|
</ReaderOverlayContextProvider>
|
||||||
</ReaderTapZoneContextProvider>
|
</ReaderTapZoneContextProvider>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
ReaderStateChapters,
|
ReaderStateChapters,
|
||||||
ReaderTransitionPageMode,
|
ReaderTransitionPageMode,
|
||||||
ReadingMode,
|
ReadingMode,
|
||||||
|
TReaderAutoScrollContext,
|
||||||
TReaderStateMangaContext,
|
TReaderStateMangaContext,
|
||||||
TReaderStateSettingsContext,
|
TReaderStateSettingsContext,
|
||||||
} from '@/modules/reader/types/Reader.types.ts';
|
} from '@/modules/reader/types/Reader.types.ts';
|
||||||
@@ -54,6 +55,7 @@ import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types
|
|||||||
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
||||||
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||||
import { isAutoWebtoonMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
import { isAutoWebtoonMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||||
|
import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||||
|
|
||||||
const BaseReader = ({
|
const BaseReader = ({
|
||||||
setTitle,
|
setTitle,
|
||||||
@@ -79,6 +81,7 @@ const BaseReader = ({
|
|||||||
setPageUrls,
|
setPageUrls,
|
||||||
setPageLoadStates,
|
setPageLoadStates,
|
||||||
setTransitionPageMode,
|
setTransitionPageMode,
|
||||||
|
cancelAutoScroll,
|
||||||
}: Pick<NavbarContextType, 'setTitle' | 'setOverride' | 'readerNavBarWidth'> &
|
}: Pick<NavbarContextType, 'setTitle' | 'setOverride' | 'readerNavBarWidth'> &
|
||||||
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
|
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
|
||||||
Pick<TReaderStateMangaContext, 'manga' | 'setManga'> &
|
Pick<TReaderStateMangaContext, 'manga' | 'setManga'> &
|
||||||
@@ -97,6 +100,7 @@ const BaseReader = ({
|
|||||||
| 'setTransitionPageMode'
|
| 'setTransitionPageMode'
|
||||||
> & {
|
> & {
|
||||||
firstPageUrl?: string;
|
firstPageUrl?: string;
|
||||||
|
cancelAutoScroll: TReaderAutoScrollContext['cancel'];
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { resumeMode } = useLocation<{
|
const { resumeMode } = useLocation<{
|
||||||
@@ -184,6 +188,8 @@ const BaseReader = ({
|
|||||||
setPageLoadStates([{ loaded: false }]);
|
setPageLoadStates([{ loaded: false }]);
|
||||||
|
|
||||||
setIsOverlayVisible(false);
|
setIsOverlayVisible(false);
|
||||||
|
|
||||||
|
cancelAutoScroll();
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
@@ -411,6 +417,7 @@ export const Reader = withPropsFrom(
|
|||||||
() => ({
|
() => ({
|
||||||
firstPageUrl: userReaderStatePagesContext().pages[0].primary.url,
|
firstPageUrl: userReaderStatePagesContext().pages[0].primary.url,
|
||||||
}),
|
}),
|
||||||
|
() => ({ cancelAutoScroll: useReaderAutoScrollContext().cancel }),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'setTitle',
|
'setTitle',
|
||||||
@@ -436,5 +443,6 @@ export const Reader = withPropsFrom(
|
|||||||
'setPageUrls',
|
'setPageUrls',
|
||||||
'setPageLoadStates',
|
'setPageLoadStates',
|
||||||
'setTransitionPageMode',
|
'setTransitionPageMode',
|
||||||
|
'cancelAutoScroll',
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const convertSettingsToMetadata = (
|
|||||||
customFilter: JSON.stringify(settings.customFilter),
|
customFilter: JSON.stringify(settings.customFilter),
|
||||||
readerWidth: JSON.stringify(settings.readerWidth),
|
readerWidth: JSON.stringify(settings.readerWidth),
|
||||||
hotkeys: JSON.stringify(settings.hotkeys),
|
hotkeys: JSON.stringify(settings.hotkeys),
|
||||||
|
autoScroll: JSON.stringify(settings.autoScroll),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag(
|
export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag(
|
||||||
@@ -117,6 +118,9 @@ const convertMetadataToSettings = (
|
|||||||
...defaultSettings.hotkeys,
|
...defaultSettings.hotkeys,
|
||||||
...(jsonSaveParse<IReaderSettings['hotkeys']>((metadata.hotkeys as string) ?? '') ?? defaultSettings.hotkeys),
|
...(jsonSaveParse<IReaderSettings['hotkeys']>((metadata.hotkeys as string) ?? '') ?? defaultSettings.hotkeys),
|
||||||
},
|
},
|
||||||
|
autoScroll:
|
||||||
|
jsonSaveParse<IReaderSettings['autoScroll']>((metadata.autoScroll as string) ?? '') ??
|
||||||
|
defaultSettings.autoScroll,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getReaderSettings = (
|
export const getReaderSettings = (
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
|||||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||||
import { TMangaReader } from '@/modules/manga/Manga.types.ts';
|
import { TMangaReader } from '@/modules/manga/Manga.types.ts';
|
||||||
|
import { useAutomaticScrolling } from '@/modules/core/hooks/useAutomaticScrolling.ts';
|
||||||
|
|
||||||
export enum ProgressBarType {
|
export enum ProgressBarType {
|
||||||
HIDDEN,
|
HIDDEN,
|
||||||
@@ -124,6 +125,13 @@ export interface IReaderSettingsGlobal {
|
|||||||
hotkeys: Record<ReaderHotkey, string[]>;
|
hotkeys: Record<ReaderHotkey, string[]>;
|
||||||
imagePreLoadAmount: number;
|
imagePreLoadAmount: number;
|
||||||
shouldUseAutoWebtoonMode: boolean;
|
shouldUseAutoWebtoonMode: boolean;
|
||||||
|
autoScroll: {
|
||||||
|
/**
|
||||||
|
* interval in seconds
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
smooth: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReaderSettingsManga {
|
export interface IReaderSettingsManga {
|
||||||
@@ -208,6 +216,7 @@ export enum ReaderHotkey {
|
|||||||
OFFSET_SPREAD_PAGES,
|
OFFSET_SPREAD_PAGES,
|
||||||
CYCLE_READING_MODE,
|
CYCLE_READING_MODE,
|
||||||
CYCLE_READING_DIRECTION,
|
CYCLE_READING_DIRECTION,
|
||||||
|
TOGGLE_AUTO_SCROLL,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReaderPagerProps
|
export interface ReaderPagerProps
|
||||||
@@ -259,3 +268,9 @@ export type TReaderStateSettingsContext = {
|
|||||||
settings: IReaderSettingsWithDefaultFlag;
|
settings: IReaderSettingsWithDefaultFlag;
|
||||||
setSettings: (settings: IReaderSettingsWithDefaultFlag) => void;
|
setSettings: (settings: IReaderSettingsWithDefaultFlag) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TReaderAutoScrollContext = ReturnType<typeof useAutomaticScrolling> & {
|
||||||
|
isActive: boolean;
|
||||||
|
scrollRef?: MutableRefObject<HTMLElement | null> | (() => void);
|
||||||
|
setScrollRef: (scrollRef?: MutableRefObject<HTMLElement | null>) => void;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user