/* * 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 { useHotkeys as useHotKeysHook, useHotkeysContext } from 'react-hotkeys-hook'; 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 } 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 { AUTO_SCROLL_SPEED, CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION, READER_PAGE_SCALE_MODE_VALUES, ReaderScrollAmount, READING_DIRECTION_VALUES, READING_MODE_VALUES, } from '@/modules/reader/constants/ReaderSettings.constants.tsx'; 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 { 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'; import { useReaderTapZoneContext } from '@/modules/reader/contexts/ReaderTapZoneContext.tsx'; const useHotkeys = (...args: Parameters): ReturnType => { const [keys, callback, options, dependencies] = args; return useHotKeysHook(keys, callback, { ...options, ...HOTKEY_SCOPES.reader }, dependencies); }; const updateSettingCycleThrough = ( updateSetting: ReturnType, deleteSetting: ReturnType, setting: Setting, value: IReaderSettings[Setting], values: IReaderSettings[Setting][], isDefault: boolean, isDefaultable: boolean, ) => { if (isDefault) { updateSetting(setting, values[0]); return; } const nextValue = getNextRotationValue(values.indexOf(value), values, isDefaultable); const isDefaultNextValue = nextValue === undefined; if (isDefaultNextValue) { deleteSetting(setting); return; } updateSetting(setting, nextValue); }; export const ReaderHotkeys = ({ scrollElementRef, }: { scrollElementRef: React.MutableRefObject; }) => { const { direction: themeDirection } = useTheme(); const readerThemeDirection = ReaderService.useGetThemeDirection(); const { enableScope, disableScope } = useHotkeysContext(); const { manga } = useReaderStateMangaContext(); const { isVisible, setIsVisible: setIsOverlayVisible } = useReaderOverlayContext(); const { hotkeys, pageScaleMode, shouldStretchPage, shouldOffsetDoubleSpreads, readingMode, readingDirection, autoScroll, } = ReaderService.useSettings(); const automaticScrolling = useReaderAutoScrollContext(); const { setShowPreview } = useReaderTapZoneContext(); const exitReader = ReaderService.useExit(); const openChapter = ReaderControls.useOpenChapter(); const openPage = ReaderControls.useOpenPage(); const updateSetting = ReaderService.useCreateUpdateSetting(manga ?? FALLBACK_MANGA); const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? FALLBACK_MANGA); useHotkeys(hotkeys[ReaderHotkey.PREVIOUS_PAGE], () => openPage('previous'), [openPage]); useHotkeys(hotkeys[ReaderHotkey.NEXT_PAGE], () => openPage('next'), [openPage]); useHotkeys( hotkeys[ReaderHotkey.SCROLL_BACKWARD], () => scrollElementRef.current && ReaderControls.scroll( ScrollOffset.BACKWARD, CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value], readingMode.value, readingDirection.value, themeDirection, scrollElementRef.current, openChapter, setIsOverlayVisible, setShowPreview, ReaderScrollAmount.SMALL, ), { preventDefault: true }, [readingMode.value, readingDirection.value, themeDirection, openChapter], ); useHotkeys( hotkeys[ReaderHotkey.SCROLL_FORWARD], () => scrollElementRef.current && ReaderControls.scroll( ScrollOffset.FORWARD, CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value], readingMode.value, readingDirection.value, themeDirection, scrollElementRef.current, openChapter, setIsOverlayVisible, setShowPreview, ReaderScrollAmount.SMALL, ), { preventDefault: true }, [readingMode.value, readingDirection.value, themeDirection, openChapter], ); useHotkeys( hotkeys[ReaderHotkey.PREVIOUS_CHAPTER], () => openChapter(getOptionForDirection('previous', 'next', readerThemeDirection)), [openChapter, readerThemeDirection], ); useHotkeys( hotkeys[ReaderHotkey.NEXT_CHAPTER], () => openChapter(getOptionForDirection('next', 'previous', readerThemeDirection)), [openChapter, readerThemeDirection], ); useHotkeys(hotkeys[ReaderHotkey.TOGGLE_MENU], () => setIsOverlayVisible(!isVisible), [isVisible]); useHotkeys( hotkeys[ReaderHotkey.CYCLE_SCALE_TYPE], () => { updateSettingCycleThrough( updateSetting, deleteSetting, 'pageScaleMode', pageScaleMode.value, READER_PAGE_SCALE_MODE_VALUES, pageScaleMode.isDefault, true, ); }, [updateSetting, deleteSetting, pageScaleMode.value, pageScaleMode.isDefault], ); useHotkeys( hotkeys[ReaderHotkey.STRETCH_IMAGE], () => updateSetting('shouldStretchPage', !shouldStretchPage.value), [updateSetting, shouldStretchPage.value], ); useHotkeys( hotkeys[ReaderHotkey.OFFSET_SPREAD_PAGES], () => updateSetting('shouldOffsetDoubleSpreads', !shouldOffsetDoubleSpreads.value), [updateSetting, shouldOffsetDoubleSpreads.value], ); useHotkeys( hotkeys[ReaderHotkey.CYCLE_READING_MODE], () => { updateSettingCycleThrough( updateSetting, deleteSetting, 'readingMode', readingMode.value, READING_MODE_VALUES, readingMode.isDefault, true, ); }, [updateSetting, deleteSetting, readingMode.value, readingMode.isDefault], ); useHotkeys( hotkeys[ReaderHotkey.CYCLE_READING_DIRECTION], () => { updateSettingCycleThrough( updateSetting, deleteSetting, 'readingDirection', readingDirection.value, READING_DIRECTION_VALUES, readingDirection.isDefault, true, ); }, [updateSetting, deleteSetting, readingDirection.value, readingDirection.isDefault], ); useHotkeys(hotkeys[ReaderHotkey.TOGGLE_AUTO_SCROLL], automaticScrolling.toggleActive, { preventDefault: true }, [ automaticScrolling.toggleActive, ]); useHotkeys( hotkeys[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE], () => updateSetting('autoScroll', { ...autoScroll, value: Math.min(AUTO_SCROLL_SPEED.max, autoScroll.value + AUTO_SCROLL_SPEED.step), }), [updateSetting, autoScroll.value], ); useHotkeys( hotkeys[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE], () => updateSetting('autoScroll', { ...autoScroll, value: Math.max(AUTO_SCROLL_SPEED.min, autoScroll.value - AUTO_SCROLL_SPEED.step), }), [updateSetting, autoScroll.value], ); useHotkeys(hotkeys[ReaderHotkey.EXIT_READER], exitReader, [exitReader]); useEffect(() => { enableScope(HotkeyScope.READER); return () => disableScope(HotkeyScope.READER); }, []); return null; };