Add "automatic scrolling"
This commit is contained in:
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,
|
||||
imagePreLoadAmount: undefined,
|
||||
shouldUseAutoWebtoonMode: undefined,
|
||||
autoScroll: undefined,
|
||||
};
|
||||
|
||||
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 { HOTKEY_SCOPES } from '@/modules/hotkeys/Hotkeys.constants.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 { getNextRotationValue } from '@/modules/core/utils/ValueRotationButton.utils.ts';
|
||||
import {
|
||||
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION,
|
||||
READER_PAGE_SCALE_MODE_VALUES,
|
||||
ReaderScrollAmount,
|
||||
READING_DIRECTION_VALUES,
|
||||
@@ -23,9 +24,10 @@ import {
|
||||
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { HotkeyScope } from '@/modules/hotkeys/Hotkeys.types.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 { 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 [keys, callback, options, dependencies] = args;
|
||||
@@ -57,14 +59,6 @@ const updateSettingCycleThrough = <Setting extends keyof IReaderSettings>(
|
||||
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 = ({
|
||||
scrollElementRef,
|
||||
}: {
|
||||
@@ -77,6 +71,7 @@ export const ReaderHotkeys = ({
|
||||
const { isVisible, setIsVisible: setIsOverlayVisible } = useReaderOverlayContext();
|
||||
const { hotkeys, pageScaleMode, shouldStretchPage, shouldOffsetDoubleSpreads, readingMode, readingDirection } =
|
||||
ReaderService.useSettings();
|
||||
const automaticScrolling = useReaderAutoScrollContext();
|
||||
|
||||
const openChapter = ReaderControls.useOpenChapter();
|
||||
const openPage = ReaderControls.useOpenPage();
|
||||
@@ -188,6 +183,10 @@ export const ReaderHotkeys = ({
|
||||
},
|
||||
[updateSetting, deleteSetting, readingDirection.value, readingDirection.isDefault],
|
||||
);
|
||||
useHotkeys(hotkeys[ReaderHotkey.TOGGLE_AUTO_SCROLL], automaticScrolling.toggleActive, { preventDefault: true }, [
|
||||
updateSetting,
|
||||
automaticScrolling.toggleActive,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
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 { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { FALLBACK_MANGA } from '@/modules/manga/Manga.constants.ts';
|
||||
import { ReaderNavBarDesktopAutoScroll } from '@/modules/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopAutoScroll.tsx';
|
||||
|
||||
const BaseReaderNavBarDesktopQuickSettings = ({
|
||||
manga,
|
||||
@@ -28,12 +29,18 @@ const BaseReaderNavBarDesktopQuickSettings = ({
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
readingDirection,
|
||||
autoScroll,
|
||||
openSettings,
|
||||
}: Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<ReaderNavBarDesktopProps, 'openSettings'> &
|
||||
Pick<
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
'readingMode' | 'shouldOffsetDoubleSpreads' | 'pageScaleMode' | 'shouldStretchPage' | 'readingDirection'
|
||||
| 'readingMode'
|
||||
| 'shouldOffsetDoubleSpreads'
|
||||
| 'pageScaleMode'
|
||||
| 'shouldStretchPage'
|
||||
| 'readingDirection'
|
||||
| 'autoScroll'
|
||||
>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -66,6 +73,10 @@ const BaseReaderNavBarDesktopQuickSettings = ({
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('readingDirection')}
|
||||
/>
|
||||
<ReaderNavBarDesktopAutoScroll
|
||||
autoScroll={autoScroll}
|
||||
setAutoScroll={(...args) => updateSetting('autoScroll', ...args)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => openSettings()}
|
||||
size="large"
|
||||
@@ -82,5 +93,13 @@ const BaseReaderNavBarDesktopQuickSettings = ({
|
||||
export const ReaderNavBarDesktopQuickSettings = withPropsFrom(
|
||||
BaseReaderNavBarDesktopQuickSettings,
|
||||
[useReaderStateMangaContext, ReaderService.useSettings],
|
||||
['manga', 'readingMode', 'shouldOffsetDoubleSpreads', 'pageScaleMode', 'shouldStretchPage', 'readingDirection'],
|
||||
[
|
||||
'manga',
|
||||
'readingMode',
|
||||
'shouldOffsetDoubleSpreads',
|
||||
'pageScaleMode',
|
||||
'shouldStretchPage',
|
||||
'readingDirection',
|
||||
'autoScroll',
|
||||
],
|
||||
);
|
||||
|
||||
@@ -84,7 +84,7 @@ const BaseReaderBottomBarMobile = ({
|
||||
<FormatListBulletedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('reader.settings.label.reader_type')}>
|
||||
<Tooltip title={t('reader.settings.title.quick_settings')}>
|
||||
<IconButton {...bindTrigger(quickSettingsPopupState)} size="large" color="inherit">
|
||||
<AppSettingsAltIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -8,21 +8,34 @@
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReaderSettingReadingMode } from '@/modules/reader/components/settings/layout/ReaderSettingReadingMode.tsx';
|
||||
import { ReaderSettingReadingDirection } from '@/modules/reader/components/settings/layout/ReaderSettingReadingDirection.tsx';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.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 { 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 = ({
|
||||
manga,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
autoScroll,
|
||||
isActive,
|
||||
toggleActive,
|
||||
}: 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);
|
||||
|
||||
if (!manga) {
|
||||
@@ -44,12 +57,21 @@ const BaseReaderBottomBarMobileQuickSettings = ({
|
||||
isDefaultable
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderBottomBarMobileQuickSettings = withPropsFrom(
|
||||
memo(BaseReaderBottomBarMobileQuickSettings),
|
||||
[useReaderStateMangaContext, ReaderService.useSettings],
|
||||
['manga', 'readingMode', 'readingDirection'],
|
||||
[useReaderStateMangaContext, ReaderService.useSettings, useReaderAutoScrollContext],
|
||||
['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 { SliderInput } from '@/modules/core/components/inputs/SliderInput.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 = ({
|
||||
settings,
|
||||
@@ -60,6 +61,10 @@ export const ReaderBehaviourSettings = ({
|
||||
checked={settings.shouldUseAutoWebtoonMode}
|
||||
onChange={(_, checked) => updateSetting('shouldUseAutoWebtoonMode', checked)}
|
||||
/>
|
||||
<ReaderSettingAutoScroll
|
||||
autoScroll={settings.autoScroll}
|
||||
setAutoScroll={(...args) => updateSetting('autoScroll', ...args)}
|
||||
/>
|
||||
<SliderInput
|
||||
label={t('reader.settings.image_preload_amount')}
|
||||
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 { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
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> = {
|
||||
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
|
||||
@@ -114,6 +115,9 @@ const BaseReaderViewer = forwardRef(
|
||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||
const isDragging = useMouseDragScroll(isContinuousReadingModeActive ? scrollElementRef : undefined);
|
||||
|
||||
const automaticScrolling = useReaderAutoScrollContext();
|
||||
useEffect(() => automaticScrolling.setScrollRef(scrollElementRef), []);
|
||||
|
||||
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
|
||||
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
|
||||
useLayoutEffect(() => {
|
||||
@@ -320,6 +324,16 @@ const BaseReaderViewer = forwardRef(
|
||||
};
|
||||
}, [isOverlayVisible]);
|
||||
|
||||
// handle auto scrolling
|
||||
useEffect(() => {
|
||||
if (isOverlayVisible) {
|
||||
automaticScrolling.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
automaticScrolling.resume();
|
||||
}, [isOverlayVisible, automaticScrolling.isPaused]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
ref={scrollElementRef}
|
||||
|
||||
@@ -13,7 +13,7 @@ import ExpandIcon from '@mui/icons-material/Expand';
|
||||
import CropOriginalIcon from '@mui/icons-material/CropOriginal';
|
||||
import { TooltipProps } from '@mui/material/Tooltip';
|
||||
import { Direction } from '@mui/material/styles';
|
||||
import { ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
import { ScrollDirection, ValueToDisplayData } from '@/modules/core/Core.types.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsGlobal,
|
||||
@@ -56,6 +56,7 @@ const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefine
|
||||
hotkeys: undefined,
|
||||
imagePreLoadAmount: undefined,
|
||||
shouldUseAutoWebtoonMode: undefined,
|
||||
autoScroll: undefined,
|
||||
};
|
||||
|
||||
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.CYCLE_READING_MODE]: ['r'],
|
||||
[ReaderHotkey.CYCLE_READING_DIRECTION]: ['t'],
|
||||
[ReaderHotkey.TOGGLE_AUTO_SCROLL]: ['space'],
|
||||
},
|
||||
imagePreLoadAmount: 5,
|
||||
shouldUseAutoWebtoonMode: true,
|
||||
autoScroll: {
|
||||
value: 5,
|
||||
smooth: true,
|
||||
},
|
||||
};
|
||||
|
||||
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 {
|
||||
SMALL = 25,
|
||||
MEDIUM = 75,
|
||||
LARGE = 95,
|
||||
}
|
||||
|
||||
@@ -262,3 +269,20 @@ export const READER_BACKGROUND_TO_COLOR = {
|
||||
[ReaderBackgroundColor.GRAY]: 'grey.200',
|
||||
[ReaderBackgroundColor.WHITE]: 'common.white',
|
||||
} 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 { ReaderStateContextProvider } from '@/modules/reader/contexts/state/ReaderStateContextProvider.tsx';
|
||||
import { ReaderScrollbarContextProvider } from '@/modules/reader/contexts/ReaderScrollbarContextProvider.tsx';
|
||||
import { ReaderAutoScrollContextProvider } from '@/modules/reader/contexts/ReaderAutoScrollContextProvider.tsx';
|
||||
|
||||
export const ReaderContextProvider = ({ children }: { children?: ReactNode }) => (
|
||||
<ReaderStateContextProvider>
|
||||
<ReaderTapZoneContextProvider>
|
||||
<ReaderOverlayContextProvider>
|
||||
<ReaderProgressBarContextProvider>
|
||||
<ReaderScrollbarContextProvider>{children}</ReaderScrollbarContextProvider>
|
||||
<ReaderScrollbarContextProvider>
|
||||
<ReaderAutoScrollContextProvider>{children}</ReaderAutoScrollContextProvider>
|
||||
</ReaderScrollbarContextProvider>
|
||||
</ReaderProgressBarContextProvider>
|
||||
</ReaderOverlayContextProvider>
|
||||
</ReaderTapZoneContextProvider>
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
ReaderStateChapters,
|
||||
ReaderTransitionPageMode,
|
||||
ReadingMode,
|
||||
TReaderAutoScrollContext,
|
||||
TReaderStateMangaContext,
|
||||
TReaderStateSettingsContext,
|
||||
} 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 { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { isAutoWebtoonMode } from '@/modules/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { useReaderAutoScrollContext } from '@/modules/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||
|
||||
const BaseReader = ({
|
||||
setTitle,
|
||||
@@ -79,6 +81,7 @@ const BaseReader = ({
|
||||
setPageUrls,
|
||||
setPageLoadStates,
|
||||
setTransitionPageMode,
|
||||
cancelAutoScroll,
|
||||
}: Pick<NavbarContextType, 'setTitle' | 'setOverride' | 'readerNavBarWidth'> &
|
||||
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
|
||||
Pick<TReaderStateMangaContext, 'manga' | 'setManga'> &
|
||||
@@ -97,6 +100,7 @@ const BaseReader = ({
|
||||
| 'setTransitionPageMode'
|
||||
> & {
|
||||
firstPageUrl?: string;
|
||||
cancelAutoScroll: TReaderAutoScrollContext['cancel'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { resumeMode } = useLocation<{
|
||||
@@ -184,6 +188,8 @@ const BaseReader = ({
|
||||
setPageLoadStates([{ loaded: false }]);
|
||||
|
||||
setIsOverlayVisible(false);
|
||||
|
||||
cancelAutoScroll();
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -411,6 +417,7 @@ export const Reader = withPropsFrom(
|
||||
() => ({
|
||||
firstPageUrl: userReaderStatePagesContext().pages[0].primary.url,
|
||||
}),
|
||||
() => ({ cancelAutoScroll: useReaderAutoScrollContext().cancel }),
|
||||
],
|
||||
[
|
||||
'setTitle',
|
||||
@@ -436,5 +443,6 @@ export const Reader = withPropsFrom(
|
||||
'setPageUrls',
|
||||
'setPageLoadStates',
|
||||
'setTransitionPageMode',
|
||||
'cancelAutoScroll',
|
||||
],
|
||||
);
|
||||
|
||||
@@ -91,6 +91,7 @@ const convertSettingsToMetadata = (
|
||||
customFilter: JSON.stringify(settings.customFilter),
|
||||
readerWidth: JSON.stringify(settings.readerWidth),
|
||||
hotkeys: JSON.stringify(settings.hotkeys),
|
||||
autoScroll: JSON.stringify(settings.autoScroll),
|
||||
});
|
||||
|
||||
export const DEFAULT_READER_SETTINGS_WITH_DEFAULT_FLAG = convertToSettingsWithDefaultFlag(
|
||||
@@ -117,6 +118,9 @@ const convertMetadataToSettings = (
|
||||
...defaultSettings.hotkeys,
|
||||
...(jsonSaveParse<IReaderSettings['hotkeys']>((metadata.hotkeys as string) ?? '') ?? defaultSettings.hotkeys),
|
||||
},
|
||||
autoScroll:
|
||||
jsonSaveParse<IReaderSettings['autoScroll']>((metadata.autoScroll as string) ?? '') ??
|
||||
defaultSettings.autoScroll,
|
||||
});
|
||||
|
||||
export const getReaderSettings = (
|
||||
|
||||
@@ -12,6 +12,7 @@ import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
|
||||
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
|
||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||
import { TMangaReader } from '@/modules/manga/Manga.types.ts';
|
||||
import { useAutomaticScrolling } from '@/modules/core/hooks/useAutomaticScrolling.ts';
|
||||
|
||||
export enum ProgressBarType {
|
||||
HIDDEN,
|
||||
@@ -124,6 +125,13 @@ export interface IReaderSettingsGlobal {
|
||||
hotkeys: Record<ReaderHotkey, string[]>;
|
||||
imagePreLoadAmount: number;
|
||||
shouldUseAutoWebtoonMode: boolean;
|
||||
autoScroll: {
|
||||
/**
|
||||
* interval in seconds
|
||||
*/
|
||||
value: number;
|
||||
smooth: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IReaderSettingsManga {
|
||||
@@ -208,6 +216,7 @@ export enum ReaderHotkey {
|
||||
OFFSET_SPREAD_PAGES,
|
||||
CYCLE_READING_MODE,
|
||||
CYCLE_READING_DIRECTION,
|
||||
TOGGLE_AUTO_SCROLL,
|
||||
}
|
||||
|
||||
export interface ReaderPagerProps
|
||||
@@ -259,3 +268,9 @@ export type TReaderStateSettingsContext = {
|
||||
settings: IReaderSettingsWithDefaultFlag;
|
||||
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