Add "automatic scrolling"

This commit is contained in:
schroda
2024-12-27 00:30:19 +01:00
parent 6b0df5329b
commit e2075a651e
18 changed files with 520 additions and 19 deletions

View 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],
);
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>
);
};

View File

@@ -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',
],
);

View File

@@ -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>

View File

@@ -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'],
);

View File

@@ -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}

View File

@@ -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);
},
},
}}
/>
</>
);
};

View File

@@ -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}

View File

@@ -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,
};

View 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);

View File

@@ -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'],
);

View File

@@ -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>

View File

@@ -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',
],
);

View File

@@ -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 = (

View File

@@ -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;
};