Add "automatic scrolling"
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user