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

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