Rename folder "modules" to "features"
This commit is contained in:
261
src/features/reader/components/ReaderHotkeys.tsx
Normal file
261
src/features/reader/components/ReaderHotkeys.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* 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 '@/features/hotkeys/Hotkeys.constants.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { IReaderSettings, ReaderHotkey } from '@/features/reader/types/Reader.types.ts';
|
||||
import { useReaderOverlayContext } from '@/features/reader/contexts/ReaderOverlayContext.tsx';
|
||||
import { getNextRotationValue } from '@/features/core/utils/ValueRotationButton.utils.ts';
|
||||
import {
|
||||
AUTO_SCROLL_SPEED,
|
||||
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION,
|
||||
READER_PAGE_SCALE_MODE_VALUES,
|
||||
READING_DIRECTION_VALUES,
|
||||
READING_MODE_VALUES,
|
||||
} from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { HotkeyScope } from '@/features/hotkeys/Hotkeys.types.ts';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import { ScrollOffset } from '@/features/core/Core.types.ts';
|
||||
import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { useReaderAutoScrollContext } from '@/features/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||
import { useReaderTapZoneContext } from '@/features/reader/contexts/ReaderTapZoneContext.tsx';
|
||||
|
||||
const useHotkeys = (...args: Parameters<typeof useHotKeysHook>): ReturnType<typeof useHotKeysHook> => {
|
||||
const [keys, callback, options, dependencies] = args;
|
||||
return useHotKeysHook(keys, callback, { ...options, ...HOTKEY_SCOPES.reader }, dependencies);
|
||||
};
|
||||
|
||||
const updateSettingCycleThrough = <Setting extends keyof IReaderSettings>(
|
||||
updateSetting: ReturnType<typeof ReaderService.useCreateUpdateSetting>,
|
||||
deleteSetting: ReturnType<typeof ReaderService.useCreateDeleteSetting>,
|
||||
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<HTMLElement | null>;
|
||||
}) => {
|
||||
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,
|
||||
scrollAmount,
|
||||
} = 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],
|
||||
() => {
|
||||
if (automaticScrolling.isActive) {
|
||||
automaticScrolling.setDirection(ScrollOffset.BACKWARD);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scrollElementRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReaderControls.scroll(
|
||||
ScrollOffset.BACKWARD,
|
||||
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value],
|
||||
readingMode.value,
|
||||
readingDirection.value,
|
||||
themeDirection,
|
||||
scrollElementRef.current,
|
||||
openChapter,
|
||||
setIsOverlayVisible,
|
||||
setShowPreview,
|
||||
scrollAmount,
|
||||
);
|
||||
},
|
||||
{ preventDefault: true },
|
||||
[
|
||||
readingMode.value,
|
||||
readingDirection.value,
|
||||
themeDirection,
|
||||
openChapter,
|
||||
scrollAmount,
|
||||
automaticScrolling.isActive,
|
||||
],
|
||||
);
|
||||
useHotkeys(
|
||||
hotkeys[ReaderHotkey.SCROLL_FORWARD],
|
||||
() => {
|
||||
if (automaticScrolling.isActive) {
|
||||
automaticScrolling.setDirection(ScrollOffset.FORWARD);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scrollElementRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReaderControls.scroll(
|
||||
ScrollOffset.FORWARD,
|
||||
CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION[readingMode.value],
|
||||
readingMode.value,
|
||||
readingDirection.value,
|
||||
themeDirection,
|
||||
scrollElementRef.current,
|
||||
openChapter,
|
||||
setIsOverlayVisible,
|
||||
setShowPreview,
|
||||
scrollAmount,
|
||||
);
|
||||
},
|
||||
{ preventDefault: true },
|
||||
[
|
||||
readingMode.value,
|
||||
readingDirection.value,
|
||||
themeDirection,
|
||||
openChapter,
|
||||
scrollAmount,
|
||||
automaticScrolling.isActive,
|
||||
],
|
||||
);
|
||||
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_DECREASE],
|
||||
() =>
|
||||
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_INCREASE],
|
||||
() =>
|
||||
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;
|
||||
};
|
||||
117
src/features/reader/components/ReaderPageNumber.tsx
Normal file
117
src/features/reader/components/ReaderPageNumber.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Box from '@mui/material/Box';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ProgressBarType,
|
||||
ReadingDirection,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { getPage } from '@/features/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { useReaderProgressBarContext } from '@/features/reader/contexts/ReaderProgressBarContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { reverseString } from '@/util/Strings.ts';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { ReaderStatePages, TReaderProgressBarContext } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
|
||||
const BaseReaderPageNumber = ({
|
||||
isDesktop,
|
||||
scrollbarXSize,
|
||||
readerNavBarWidth,
|
||||
isMaximized,
|
||||
currentPageIndex,
|
||||
pages,
|
||||
totalPages,
|
||||
progressBarType,
|
||||
shouldShowPageNumber,
|
||||
readingDirection,
|
||||
}: Pick<TReaderScrollbarContext, 'scrollbarXSize'> &
|
||||
Pick<ReturnType<typeof ReaderService.useOverlayMode>, 'isDesktop'> &
|
||||
Pick<NavbarContextType, 'readerNavBarWidth'> &
|
||||
Pick<TReaderProgressBarContext, 'isMaximized'> &
|
||||
Pick<ReaderStatePages, 'currentPageIndex' | 'pages' | 'totalPages'> &
|
||||
Pick<IReaderSettings, 'progressBarType' | 'shouldShowPageNumber' | 'readingDirection'>) => {
|
||||
const pageName = useMemo(() => {
|
||||
const currentPageName = getPage(currentPageIndex, pages).name;
|
||||
const SEPARATOR = '/';
|
||||
const tmpPageName = `${currentPageName}${SEPARATOR}${totalPages}`;
|
||||
|
||||
return readingDirection === ReadingDirection.LTR ? tmpPageName : reverseString(tmpPageName, SEPARATOR);
|
||||
}, [currentPageIndex, pages, totalPages, readingDirection]);
|
||||
|
||||
if (!shouldShowPageNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMaximized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDesktop && progressBarType === ProgressBarType.STANDARD) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMaximized && progressBarType === ProgressBarType.HIDDEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
left: readerNavBarWidth,
|
||||
right: 0,
|
||||
bottom: (theme) => `max(calc(${theme.spacing(1)} + ${scrollbarXSize}px), env(safe-area-inset-bottom))`,
|
||||
alignItems: 'center',
|
||||
transition: (theme) => `left 0.${theme.transitions.duration.shortest}s`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
p: 0.5,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: 'white' }}>{pageName}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderPageNumber = withPropsFrom(
|
||||
BaseReaderPageNumber,
|
||||
[
|
||||
ReaderService.useOverlayMode,
|
||||
useReaderScrollbarContext,
|
||||
useNavBarContext,
|
||||
useReaderProgressBarContext,
|
||||
userReaderStatePagesContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
],
|
||||
[
|
||||
'isDesktop',
|
||||
'scrollbarXSize',
|
||||
'readerNavBarWidth',
|
||||
'isMaximized',
|
||||
'currentPageIndex',
|
||||
'pages',
|
||||
'totalPages',
|
||||
'progressBarType',
|
||||
'shouldShowPageNumber',
|
||||
'readingDirection',
|
||||
],
|
||||
);
|
||||
49
src/features/reader/components/ReaderRGBAFilter.tsx
Normal file
49
src/features/reader/components/ReaderRGBAFilter.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
|
||||
const BaseReaderRGBAFilter = ({
|
||||
readerNavBarWidth,
|
||||
customFilter: {
|
||||
rgba: {
|
||||
value: { red, green, blue, alpha, blendMode },
|
||||
enabled,
|
||||
},
|
||||
},
|
||||
}: Pick<NavbarContextType, 'readerNavBarWidth'> & Pick<IReaderSettings, 'customFilter'>) => {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: readerNavBarWidth,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'none',
|
||||
background: `rgba(${red} ${green} ${blue} / ${alpha}%)`,
|
||||
mixBlendMode: `${blendMode}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderRGBAFilter = withPropsFrom(
|
||||
BaseReaderRGBAFilter,
|
||||
[useNavBarContext, ReaderService.useSettingsWithoutDefaultFlag],
|
||||
['readerNavBarWidth', 'customFilter'],
|
||||
);
|
||||
78
src/features/reader/components/TapZoneLayout.tsx
Normal file
78
src/features/reader/components/TapZoneLayout.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useReaderTapZoneContext } from '@/features/reader/contexts/ReaderTapZoneContext.tsx';
|
||||
import { ReaderTapZoneService } from '@/features/reader/services/ReaderTapZoneService.ts';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { IReaderSettings, ReadingDirection } from '@/features/reader/types/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { TReaderTapZoneContext } from '@/features/reader/types/TapZoneLayout.types.ts';
|
||||
|
||||
const CANVAS_ID = 'reader-tap-zone-layout-canvas';
|
||||
|
||||
const BaseTapZoneLayout = ({
|
||||
readerNavBarWidth,
|
||||
showPreview,
|
||||
tapZoneLayout,
|
||||
tapZoneInvertMode,
|
||||
readingDirection,
|
||||
}: Pick<NavbarContextType, 'readerNavBarWidth'> &
|
||||
Pick<TReaderTapZoneContext, 'showPreview'> &
|
||||
Pick<IReaderSettings, 'tapZoneLayout' | 'tapZoneInvertMode' | 'readingDirection'>) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
const [tapZoneLayoutElement, setTapZoneLayoutElement] = useState<HTMLDivElement | null>(null);
|
||||
useResizeObserver(
|
||||
tapZoneLayoutElement,
|
||||
useCallback(() => {
|
||||
setWidth(tapZoneLayoutElement?.clientWidth ?? 0);
|
||||
setHeight(tapZoneLayoutElement?.clientHeight ?? 0);
|
||||
}, [tapZoneLayoutElement]),
|
||||
);
|
||||
|
||||
const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout, width, height, theme.typography.h3, {
|
||||
vertical: tapZoneInvertMode.vertical,
|
||||
horizontal: tapZoneInvertMode.horizontal,
|
||||
isRTL: readingDirection === ReadingDirection.RTL,
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const canvasContainerElement = document.getElementById(CANVAS_ID);
|
||||
canvasContainerElement?.replaceChildren(canvas);
|
||||
}, [canvas, showPreview]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={setTapZoneLayoutElement}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: readerNavBarWidth,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{showPreview && <div id={CANVAS_ID} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const TapZoneLayout = withPropsFrom(
|
||||
BaseTapZoneLayout,
|
||||
[useNavBarContext, useReaderTapZoneContext, ReaderService.useSettingsWithoutDefaultFlag],
|
||||
['readerNavBarWidth', 'showPreview', 'tapZoneLayout', 'tapZoneInvertMode', 'readingDirection'],
|
||||
);
|
||||
69
src/features/reader/components/overlay/ReaderOverlay.tsx
Normal file
69
src/features/reader/components/overlay/ReaderOverlay.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { BaseReaderOverlayProps, MobileHeaderProps } from '@/features/reader/types/ReaderOverlay.types.ts';
|
||||
import { ReaderSettings } from '@/features/reader/components/settings/ReaderSettings.tsx';
|
||||
import { ReaderPageNumber } from '@/features/reader/components/ReaderPageNumber.tsx';
|
||||
import { StandardReaderProgressBar } from '@/features/reader/components/overlay/progress-bar/variants/StandardReaderProgressBar.tsx';
|
||||
import { ReaderNavBarDesktop } from '@/features/reader/components/overlay/navigation/desktop/ReaderNavBarDesktop.tsx';
|
||||
import { ReaderOverlayHeaderMobile } from '@/features/reader/components/overlay/ReaderOverlayHeaderMobile.tsx';
|
||||
import { ReaderBottomBarMobile } from '@/features/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
|
||||
const BaseReaderOverlay = ({
|
||||
isVisible,
|
||||
isDesktop,
|
||||
isMobile,
|
||||
}: BaseReaderOverlayProps &
|
||||
MobileHeaderProps &
|
||||
Pick<ReturnType<typeof ReaderService.useOverlayMode>, 'isDesktop' | 'isMobile'>) => {
|
||||
const [areSettingsOpen, setAreSettingsOpen] = useState(false);
|
||||
|
||||
const [mobileHeaderHeight, setMobileHeaderHeight] = useState(0);
|
||||
const mobileHeaderRef = useRef<HTMLDivElement>(null);
|
||||
useResizeObserver(
|
||||
mobileHeaderRef,
|
||||
useCallback(() => setMobileHeaderHeight(mobileHeaderRef.current?.clientHeight ?? 0), [isMobile]),
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'absolute', width: '100%', height: '100%', pointerEvents: 'none', zIndex: 1 }}>
|
||||
{isDesktop && (
|
||||
<>
|
||||
<StandardReaderProgressBar />
|
||||
<ReaderNavBarDesktop isVisible={isVisible} openSettings={() => setAreSettingsOpen(true)} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<>
|
||||
<ReaderOverlayHeaderMobile ref={mobileHeaderRef} isVisible={isVisible} />
|
||||
<ReaderBottomBarMobile
|
||||
openSettings={() => setAreSettingsOpen(true)}
|
||||
isVisible={isVisible}
|
||||
topOffset={mobileHeaderHeight}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ReaderSettings isOpen={areSettingsOpen} close={() => setAreSettingsOpen(false)} />
|
||||
|
||||
<ReaderPageNumber />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderOverlay = withPropsFrom(
|
||||
memo(BaseReaderOverlay),
|
||||
[ReaderService.useOverlayMode],
|
||||
['isDesktop', 'isMobile'],
|
||||
);
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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 IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { bindMenu, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import Link from '@mui/material/Link';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { forwardRef, memo } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { TypographyMaxLines } from '@/features/core/components/texts/TypographyMaxLines.tsx';
|
||||
import { makeToast } from '@/features/core/utils/Toast.ts';
|
||||
import { MobileHeaderProps } from '@/features/reader/types/ReaderOverlay.types.ts';
|
||||
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder';
|
||||
import { AppRoutes } from '@/features/core/AppRoute.constants.ts';
|
||||
import {
|
||||
ReaderStateChapters,
|
||||
TReaderScrollbarContext,
|
||||
TReaderStateMangaContext,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { ReaderLibraryButton } from '@/features/reader/components/overlay/navigation/ReaderLibraryButton.tsx';
|
||||
import { ReaderBookmarkButton } from '@/features/reader/components/overlay/navigation/ReaderBookmarkButton.tsx';
|
||||
import { FALLBACK_CHAPTER } from '@/features/chapter/Chapter.constants.ts';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { ReaderExitButton } from '@/features/reader/components/overlay/navigation/ReaderExitButton.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
|
||||
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
|
||||
|
||||
const BaseReaderOverlayHeaderMobile = forwardRef<
|
||||
HTMLDivElement,
|
||||
MobileHeaderProps &
|
||||
Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<ReaderStateChapters, 'currentChapter'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarYSize'>
|
||||
>(({ isVisible, manga, currentChapter, scrollbarYSize }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const popupState = usePopupState({ popupId: 'reader-overlay-more-menu', variant: 'popover' });
|
||||
|
||||
const { id: mangaId, title } = manga ?? DEFAULT_MANGA;
|
||||
const { id: chapterId, name, realUrl, isBookmarked } = currentChapter ?? FALLBACK_CHAPTER;
|
||||
|
||||
return (
|
||||
<Slide direction="down" in={isVisible} ref={ref}>
|
||||
<Stack
|
||||
sx={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: `${scrollbarYSize}px`,
|
||||
p: 2,
|
||||
pt: (theme) => `max(env(safe-area-inset-top), ${theme.spacing(2)})`,
|
||||
backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.95),
|
||||
pointerEvents: 'all',
|
||||
boxShadow: 2,
|
||||
}}
|
||||
>
|
||||
<ReaderExitButton />
|
||||
<Stack sx={{ flexGrow: 1 }}>
|
||||
{manga && currentChapter ? (
|
||||
<>
|
||||
<CustomTooltip title={title}>
|
||||
<TypographyMaxLines lines={1} component="h1" variant="h5">
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={AppRoutes.manga.path(mangaId)}
|
||||
sx={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={name}>
|
||||
<TypographyMaxLines lines={1}>{name}</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
</>
|
||||
) : (
|
||||
<LoadingPlaceholder />
|
||||
)}
|
||||
</Stack>
|
||||
<ReaderLibraryButton />
|
||||
<ReaderBookmarkButton id={chapterId} isBookmarked={isBookmarked} />
|
||||
<IconButton {...bindTrigger(popupState)} color="inherit">
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
disabled={!realUrl}
|
||||
href={realUrl ?? ''}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('global.button.open_browser')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
disabled={!realUrl}
|
||||
href={realUrl ? requestManager.getWebviewUrl(realUrl) : ''}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('global.button.open_webview')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!realUrl}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(title);
|
||||
makeToast(t('global.label.copied_clipboard'), 'info');
|
||||
}}
|
||||
>
|
||||
{t('global.label.share')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
</Slide>
|
||||
);
|
||||
});
|
||||
|
||||
export const ReaderOverlayHeaderMobile = withPropsFrom(
|
||||
memo(BaseReaderOverlayHeaderMobile),
|
||||
[useReaderStateMangaContext, useReaderStateChaptersContext, useReaderScrollbarContext],
|
||||
['manga', 'currentChapter', 'scrollbarYSize'],
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { memo } from 'react';
|
||||
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { ChapterAction, TChapterReader } from '@/features/chapter/Chapter.types.ts';
|
||||
import { CHAPTER_ACTION_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
|
||||
|
||||
const BaseReaderBookmarkButton = ({ id, isBookmarked }: Pick<TChapterReader, 'id' | 'isBookmarked'>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const bookmarkAction: Extract<ChapterAction, 'unbookmark' | 'bookmark'> = isBookmarked ? 'unbookmark' : 'bookmark';
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION[bookmarkAction].action.single)}>
|
||||
<IconButton onClick={() => Chapters.performAction(bookmarkAction, [id], {})} color="inherit">
|
||||
{isBookmarked ? <BookmarkIcon /> : <BookmarkBorderIcon />}
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderBookmarkButton = memo(BaseReaderBookmarkButton);
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { Virtuoso, VirtuosoProps } from 'react-virtuoso';
|
||||
import { useMemo } from 'react';
|
||||
import { ReaderStateChapters } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ChapterListCard } from '@/features/chapter/components/cards/ChapterListCard.tsx';
|
||||
|
||||
const onSelectNoop = () => {};
|
||||
|
||||
export const ReaderChapterList = ({
|
||||
currentChapter,
|
||||
chapters,
|
||||
style,
|
||||
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter'> & Pick<VirtuosoProps<any, any>, 'style'>) => {
|
||||
const currentChapterIndex = useMemo(
|
||||
() => currentChapter && chapters.findIndex((chapter) => chapter.id === currentChapter.id),
|
||||
[currentChapter, chapters],
|
||||
);
|
||||
|
||||
return (
|
||||
<Virtuoso
|
||||
style={{
|
||||
height: `calc(${chapters.length} * 100px)`,
|
||||
...style,
|
||||
}}
|
||||
initialTopMostItemIndex={currentChapterIndex ?? 0}
|
||||
totalCount={chapters.length}
|
||||
computeItemKey={(index) => chapters[index].id}
|
||||
itemContent={(index) => (
|
||||
<ChapterListCard
|
||||
index={index}
|
||||
chapters={chapters}
|
||||
isSortDesc
|
||||
mode="reader"
|
||||
showChapterNumber={false}
|
||||
selected={null}
|
||||
onSelect={onSelectNoop}
|
||||
selectable={false}
|
||||
isActiveChapter={index === currentChapterIndex}
|
||||
/>
|
||||
)}
|
||||
increaseViewportBy={400}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ArrowBack from '@mui/icons-material/ArrowBack';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import { memo } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
|
||||
const BaseReaderExitButton = ({ exit }: { exit: ReturnType<typeof ReaderService.useExit> }) => {
|
||||
const { t } = useTranslation();
|
||||
const getOptionForDirection = useGetOptionForDirection();
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t('reader.button.exit')}>
|
||||
<IconButton sx={{ marginRight: 2 }} onClick={exit} color="inherit">
|
||||
{getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)}
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderExitButton = withPropsFrom(
|
||||
memo(BaseReaderExitButton),
|
||||
[() => ({ exit: ReaderService.useExit() })],
|
||||
['exit'],
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||
import { memo } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { useManageMangaLibraryState } from '@/features/manga/hooks/useManageMangaLibraryState.tsx';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { TReaderStateMangaContext } from '@/features/reader/types/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
|
||||
const ACTION_FALLBACK_MANGA = {
|
||||
...FALLBACK_MANGA,
|
||||
title: 'Fallback',
|
||||
inLibrary: false,
|
||||
};
|
||||
|
||||
const BaseReaderLibraryButton = ({ manga }: Pick<TReaderStateMangaContext, 'manga'>) => {
|
||||
const { inLibrary } = manga ?? ACTION_FALLBACK_MANGA;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { CategorySelectComponent, updateLibraryState } = useManageMangaLibraryState(
|
||||
manga ?? ACTION_FALLBACK_MANGA,
|
||||
true,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomTooltip
|
||||
title={inLibrary ? t('manga.action.library.remove.label.action') : t('manga.button.add_to_library')}
|
||||
>
|
||||
<IconButton onClick={updateLibraryState} color="inherit">
|
||||
{inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
|
||||
{CategorySelectComponent}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderLibraryButton = withPropsFrom(
|
||||
memo(BaseReaderLibraryButton),
|
||||
[useReaderStateMangaContext],
|
||||
['manga'],
|
||||
);
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { ReaderNavBarDesktopProps } from '@/features/reader/types/ReaderOverlay.types.ts';
|
||||
import { ReaderNavContainer } from '@/features/reader/components/overlay/navigation/desktop/ReaderNavContainer.tsx';
|
||||
import { ReaderNavBarDesktopMetadata } from '@/features/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopMetadata.tsx';
|
||||
import { ReaderNavBarDesktopPageNavigation } from '@/features/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopPageNavigation.tsx';
|
||||
import { ReaderNavBarDesktopChapterNavigation } from '@/features/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopChapterNavigation.tsx';
|
||||
import { ReaderNavBarDesktopQuickSettings } from '@/features/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopQuickSettings.tsx';
|
||||
import { ReaderNavBarDesktopActions } from '@/features/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopActions.tsx';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderStateChapters,
|
||||
TReaderStateMangaContext,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { ReaderExitButton } from '@/features/reader/components/overlay/navigation/ReaderExitButton.tsx';
|
||||
|
||||
const useGetPreviousNavBarStaticValue = (isVisible: boolean, isStaticNav: boolean) => {
|
||||
const wasNavBarStaticRef = useRef(isStaticNav);
|
||||
const wasNavBarStaticPreviousRef = useRef(isStaticNav);
|
||||
|
||||
const resetWasNavBarStaticValue = wasNavBarStaticPreviousRef.current !== wasNavBarStaticRef.current && !isVisible;
|
||||
if (resetWasNavBarStaticValue) {
|
||||
wasNavBarStaticRef.current = false;
|
||||
}
|
||||
|
||||
const didNavBarStaticValueChange = wasNavBarStaticPreviousRef.current !== isStaticNav;
|
||||
if (didNavBarStaticValueChange) {
|
||||
wasNavBarStaticRef.current = wasNavBarStaticPreviousRef.current;
|
||||
wasNavBarStaticPreviousRef.current = isStaticNav;
|
||||
}
|
||||
|
||||
return wasNavBarStaticRef.current;
|
||||
};
|
||||
|
||||
const BaseReaderNavBarDesktop = ({
|
||||
isVisible,
|
||||
openSettings,
|
||||
setReaderNavBarWidth,
|
||||
manga,
|
||||
chapters,
|
||||
currentChapter,
|
||||
previousChapter,
|
||||
nextChapter,
|
||||
isStaticNav,
|
||||
}: ReaderNavBarDesktopProps &
|
||||
Pick<NavbarContextType, 'setReaderNavBarWidth'> &
|
||||
Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<ReaderStateChapters, 'currentChapter' | 'previousChapter' | 'nextChapter' | 'chapters'> &
|
||||
Pick<IReaderSettings, 'isStaticNav'>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateReaderSettings = ReaderService.useCreateUpdateSetting(manga ?? FALLBACK_MANGA);
|
||||
|
||||
const [navBarElement, setNavBarElement] = useState<HTMLDivElement | null>();
|
||||
useResizeObserver(
|
||||
navBarElement,
|
||||
useCallback(() => {
|
||||
if (!isStaticNav) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReaderNavBarWidth(navBarElement!.offsetWidth);
|
||||
}, [navBarElement, isStaticNav]),
|
||||
);
|
||||
useLayoutEffect(() => () => setReaderNavBarWidth(0), []);
|
||||
|
||||
const wasNavBarStatic = useGetPreviousNavBarStaticValue(isVisible, isStaticNav);
|
||||
const changedNavBarStaticValue = wasNavBarStatic && isVisible;
|
||||
const drawerTransitionDuration = changedNavBarStaticValue ? 0 : undefined;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
variant={isStaticNav ? 'permanent' : 'persistent'}
|
||||
open={isVisible || isStaticNav}
|
||||
transitionDuration={drawerTransitionDuration}
|
||||
SlideProps={{
|
||||
unmountOnExit: true,
|
||||
}}
|
||||
PaperProps={{
|
||||
ref: (ref: HTMLDivElement | null) => setNavBarElement(ref),
|
||||
}}
|
||||
>
|
||||
<ReaderNavContainer sx={{ backgroundColor: 'background.paper', pointerEvents: 'all' }}>
|
||||
<Stack sx={{ p: 2, gap: 2, backgroundColor: 'action.hover' }}>
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<ReaderExitButton />
|
||||
<CustomTooltip title={t('reader.settings.label.static_navigation')}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setReaderNavBarWidth(0);
|
||||
updateReaderSettings('isStaticNav', !isStaticNav);
|
||||
}}
|
||||
color={isStaticNav ? 'primary' : 'inherit'}
|
||||
>
|
||||
{isStaticNav ? <PushPinIcon /> : <PushPinOutlinedIcon />}
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
</Stack>
|
||||
{manga && currentChapter ? (
|
||||
<>
|
||||
<ReaderNavBarDesktopMetadata
|
||||
mangaId={manga.id}
|
||||
mangaTitle={manga.title}
|
||||
chapterTitle={currentChapter.name}
|
||||
scanlator={currentChapter.scanlator}
|
||||
/>
|
||||
<ReaderNavBarDesktopActions />
|
||||
</>
|
||||
) : (
|
||||
<LoadingPlaceholder />
|
||||
)}
|
||||
</Stack>
|
||||
<Stack sx={{ p: 2, gap: 2 }}>
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<ReaderNavBarDesktopPageNavigation />
|
||||
<ReaderNavBarDesktopChapterNavigation
|
||||
chapters={chapters}
|
||||
currentChapter={currentChapter}
|
||||
nextChapter={nextChapter}
|
||||
previousChapter={previousChapter}
|
||||
/>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<ReaderNavBarDesktopQuickSettings openSettings={openSettings} />
|
||||
</Stack>
|
||||
</ReaderNavContainer>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderNavBarDesktop = withPropsFrom(
|
||||
memo(BaseReaderNavBarDesktop),
|
||||
[
|
||||
useNavBarContext,
|
||||
useReaderStateMangaContext,
|
||||
useReaderStateChaptersContext,
|
||||
userReaderStatePagesContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
],
|
||||
['setReaderNavBarWidth', 'manga', 'chapters', 'currentChapter', 'previousChapter', 'nextChapter', 'isStaticNav'],
|
||||
);
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { ReaderStateChapters } from '@/features/reader/types/Reader.types.ts';
|
||||
import { DownloadStateIndicator } from '@/features/core/components/downloads/DownloadStateIndicator.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { ReaderLibraryButton } from '@/features/reader/components/overlay/navigation/ReaderLibraryButton.tsx';
|
||||
import { ReaderBookmarkButton } from '@/features/reader/components/overlay/navigation/ReaderBookmarkButton.tsx';
|
||||
import { CHAPTER_ACTION_TO_TRANSLATION, FALLBACK_CHAPTER } from '@/features/chapter/Chapter.constants.ts';
|
||||
import { IconBrowser } from '@/assets/icons/IconBrowser.tsx';
|
||||
import { IconWebView } from '@/assets/icons/IconWebView.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
|
||||
const DownloadButton = ({ currentChapter }: Required<Pick<ReaderStateChapters, 'currentChapter'>>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const downloadStatus = Chapters.useDownloadStatusFromCache(currentChapter?.id ?? -1);
|
||||
|
||||
if (currentChapter && Chapters.isDownloaded(currentChapter)) {
|
||||
return (
|
||||
<CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION.delete.action.single)}>
|
||||
<IconButton onClick={() => Chapters.performAction('delete', [currentChapter.id], {})} color="inherit">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (downloadStatus) {
|
||||
return <DownloadStateIndicator chapterId={downloadStatus.chapter.id} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION.download.action.single)} disabled={!currentChapter}>
|
||||
<IconButton
|
||||
disabled={!currentChapter}
|
||||
onClick={() => Chapters.performAction('download', [currentChapter?.id ?? -1], {})}
|
||||
color="inherit"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BaseReaderNavBarDesktopActions = memo(
|
||||
({
|
||||
currentChapter,
|
||||
pageLoadStates,
|
||||
setPageLoadStates,
|
||||
setRetryFailedPagesKeyPrefix,
|
||||
}: Required<Pick<ReaderStateChapters, 'currentChapter'>> &
|
||||
Pick<ReaderStatePages, 'pageLoadStates' | 'setPageLoadStates' | 'setRetryFailedPagesKeyPrefix'>) => {
|
||||
const { id, isBookmarked, realUrl } = currentChapter ?? FALLBACK_CHAPTER;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pageRetryKeyPrefix = useRef<number>(0);
|
||||
|
||||
const haveSomePagesFailedToLoad = useMemo(
|
||||
() => pageLoadStates.some((pageLoadState) => pageLoadState.error),
|
||||
[pageLoadStates],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'center', gap: 1 }}>
|
||||
<ReaderLibraryButton />
|
||||
<ReaderBookmarkButton id={id} isBookmarked={isBookmarked} />
|
||||
<CustomTooltip title={t('reader.button.retry_load_pages')} disabled={!haveSomePagesFailedToLoad}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setPageLoadStates((statePageLoadStates) =>
|
||||
statePageLoadStates.map((pageLoadState) => ({
|
||||
url: pageLoadState.url,
|
||||
loaded: pageLoadState.loaded,
|
||||
})),
|
||||
);
|
||||
setRetryFailedPagesKeyPrefix(`${pageRetryKeyPrefix.current}`);
|
||||
pageRetryKeyPrefix.current = (pageRetryKeyPrefix.current + 1) % 1000;
|
||||
}}
|
||||
disabled={!haveSomePagesFailedToLoad}
|
||||
color="inherit"
|
||||
>
|
||||
<ReplayIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<DownloadButton currentChapter={currentChapter} />
|
||||
<CustomTooltip title={t('global.button.open_browser')} disabled={!realUrl}>
|
||||
<IconButton
|
||||
disabled={!realUrl}
|
||||
href={realUrl ?? ''}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
color="inherit"
|
||||
>
|
||||
<IconBrowser />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('global.button.open_webview')} disabled={!realUrl}>
|
||||
<IconButton
|
||||
disabled={!realUrl}
|
||||
href={realUrl ? requestManager.getWebviewUrl(realUrl) : ''}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
color="inherit"
|
||||
>
|
||||
<IconWebView />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ReaderNavBarDesktopActions = withPropsFrom(
|
||||
BaseReaderNavBarDesktopActions,
|
||||
[useReaderStateChaptersContext, userReaderStatePagesContext],
|
||||
['currentChapter', 'pageLoadStates', 'setPageLoadStates', 'setRetryFailedPagesKeyPrefix'],
|
||||
);
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { memo, useLayoutEffect } from 'react';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { bindPopover, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Select } from '@/features/core/components/inputs/Select.tsx';
|
||||
import { ReaderChapterList } from '@/features/reader/components/overlay/navigation/ReaderChapterList.tsx';
|
||||
import { ReaderNavBarDesktopNextPreviousButton } from '@/features/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopNextPreviousButton.tsx';
|
||||
import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import { ReaderStateChapters } from '@/features/reader/types/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
|
||||
const BaseReaderNavBarDesktopChapterNavigation = ({
|
||||
currentChapter,
|
||||
previousChapter,
|
||||
nextChapter,
|
||||
chapters = [],
|
||||
readerThemeDirection,
|
||||
openChapter,
|
||||
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter' | 'previousChapter' | 'nextChapter'> & {
|
||||
readerThemeDirection: ReturnType<typeof ReaderService.useGetThemeDirection>;
|
||||
openChapter: ReturnType<typeof ReaderControls.useOpenChapter>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const popupState = usePopupState({ variant: 'popover', popupId: 'reader-nav-bar-desktop-chapter-list' });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
popupState.close();
|
||||
}, [currentChapter?.id]);
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
type="previous"
|
||||
title={t(
|
||||
getOptionForDirection(
|
||||
'reader.button.previous_chapter',
|
||||
'reader.button.next_chapter',
|
||||
readerThemeDirection,
|
||||
),
|
||||
)}
|
||||
onClick={() => {
|
||||
openChapter(getOptionForDirection('previous', 'next', readerThemeDirection));
|
||||
}}
|
||||
disabled={getOptionForDirection(!previousChapter, !nextChapter, readerThemeDirection)}
|
||||
/>
|
||||
<FormControl sx={{ flexBasis: '70%', flexGrow: 0, flexShrink: 0 }}>
|
||||
<InputLabel id="reader-nav-bar-desktop-chapter-select">{t('chapter.title_one')}</InputLabel>
|
||||
<Select
|
||||
{...bindTrigger(popupState)}
|
||||
open={popupState.isOpen}
|
||||
value={currentChapter?.id ?? 0}
|
||||
// hide actual select menu
|
||||
MenuProps={{ sx: { visibility: 'hidden' } }}
|
||||
label={t('chapter.title_one')}
|
||||
labelId="reader-nav-bar-desktop-chapter-select"
|
||||
>
|
||||
{/* hacky way to use the select component with a custom menu, the only possible value that is needed is the current chapter */}
|
||||
<MenuItem key={currentChapter?.id} value={currentChapter?.id ?? 0}>
|
||||
{currentChapter ? `#${currentChapter.chapterNumber} ${currentChapter.name}` : ''}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
component={Link}
|
||||
type="next"
|
||||
title={t(
|
||||
getOptionForDirection(
|
||||
'reader.button.next_chapter',
|
||||
'reader.button.previous_chapter',
|
||||
readerThemeDirection,
|
||||
),
|
||||
)}
|
||||
onClick={() => {
|
||||
openChapter(getOptionForDirection('next', 'previous', readerThemeDirection));
|
||||
}}
|
||||
disabled={getOptionForDirection(!nextChapter, !previousChapter, readerThemeDirection)}
|
||||
/>
|
||||
<Popover
|
||||
{...bindPopover(popupState)}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<ReaderChapterList
|
||||
style={{
|
||||
width: '500px',
|
||||
maxWidth: '90vw',
|
||||
minHeight: '150px',
|
||||
maxHeight: '300px',
|
||||
}}
|
||||
currentChapter={currentChapter}
|
||||
chapters={chapters}
|
||||
/>
|
||||
</Box>
|
||||
</Popover>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderNavBarDesktopChapterNavigation = withPropsFrom(
|
||||
memo(BaseReaderNavBarDesktopChapterNavigation),
|
||||
[
|
||||
() => ({
|
||||
readerThemeDirection: ReaderService.useGetThemeDirection(),
|
||||
}),
|
||||
() => ({
|
||||
openChapter: ReaderControls.useOpenChapter(),
|
||||
}),
|
||||
],
|
||||
['readerThemeDirection', 'openChapter'],
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import Link from '@mui/material/Link';
|
||||
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { memo } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { TypographyMaxLines } from '@/features/core/components/texts/TypographyMaxLines.tsx';
|
||||
import { AppRoutes } from '@/features/core/AppRoute.constants.ts';
|
||||
|
||||
export const ReaderNavBarDesktopMetadata = memo(
|
||||
({
|
||||
mangaId,
|
||||
mangaTitle,
|
||||
chapterTitle,
|
||||
scanlator,
|
||||
}: {
|
||||
mangaId: number;
|
||||
mangaTitle: string;
|
||||
chapterTitle: string;
|
||||
scanlator?: string | null;
|
||||
}) => (
|
||||
<Stack>
|
||||
<CustomTooltip title={mangaTitle} placement="right">
|
||||
<TypographyMaxLines lines={3} variant="h6" component="h1" sx={{ textAlign: 'center' }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={AppRoutes.manga.path(mangaId)}
|
||||
sx={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
{mangaTitle}
|
||||
</Link>
|
||||
</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={chapterTitle} placement="right">
|
||||
<TypographyMaxLines lines={4} variant="body1" component="h2" sx={{ textAlign: 'center' }}>
|
||||
{chapterTitle}
|
||||
</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
{scanlator && (
|
||||
<CustomTooltip title={scanlator} placement="right">
|
||||
<TypographyMaxLines
|
||||
lines={4}
|
||||
variant="body2"
|
||||
component="h3"
|
||||
color="textDisabled"
|
||||
sx={{ textAlign: 'center' }}
|
||||
>
|
||||
{scanlator}
|
||||
</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
</Stack>
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 { ComponentProps } from 'react';
|
||||
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { CustomButtonIcon } from '@/features/core/components/buttons/CustomButtonIcon.tsx';
|
||||
|
||||
export const ReaderNavBarDesktopNextPreviousButton = ({
|
||||
title,
|
||||
type,
|
||||
disabled,
|
||||
...customIconButtonProps
|
||||
}: Omit<ComponentProps<typeof CustomButtonIcon>, 'children'> & {
|
||||
title: string;
|
||||
type: 'previous' | 'next';
|
||||
}) => (
|
||||
<CustomTooltip title={title} disabled={disabled}>
|
||||
<CustomButtonIcon sx={{ flexBasis: '15%' }} variant="contained" disabled={disabled} {...customIconButtonProps}>
|
||||
{type === 'previous' ? <KeyboardArrowLeftIcon /> : <KeyboardArrowRightIcon />}
|
||||
</CustomButtonIcon>
|
||||
</CustomTooltip>
|
||||
);
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { memo, useMemo } from 'react';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { Select } from '@/features/core/components/inputs/Select.tsx';
|
||||
import { getNextIndexFromPage, getPage } from '@/features/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderNavBarDesktopNextPreviousButton } from '@/features/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopNextPreviousButton.tsx';
|
||||
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
|
||||
const BaseReaderNavBarDesktopPageNavigation = ({
|
||||
currentPageIndex,
|
||||
pages,
|
||||
readingDirection,
|
||||
openPage,
|
||||
}: Pick<ReaderStatePages, 'currentPageIndex' | 'pages'> &
|
||||
Pick<IReaderSettings, 'readingDirection'> & {
|
||||
openPage: ReturnType<typeof ReaderControls.useOpenPage>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const getOptionForDirection = useGetOptionForDirection();
|
||||
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
|
||||
|
||||
const direction = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
type="previous"
|
||||
title={t(getOptionForDirection('reader.button.previous_page', 'reader.button.next_page', direction))}
|
||||
disabled={getOptionForDirection(
|
||||
!currentPage.primary.index,
|
||||
getNextIndexFromPage(currentPage) === getNextIndexFromPage(pages.slice(-1)[0]),
|
||||
direction,
|
||||
)}
|
||||
onClick={() => openPage('previous', undefined, false)}
|
||||
/>
|
||||
<FormControl sx={{ flexBasis: '70%', flexGrow: 0, flexShrink: 0 }}>
|
||||
<InputLabel id="reader-nav-bar-desktop-page-select">{t('reader.page_info.label.page')}</InputLabel>
|
||||
<Select
|
||||
labelId="reader-nav-bar-desktop-page-select"
|
||||
label={t('reader.page_info.label.page')}
|
||||
value={getNextIndexFromPage(currentPage)}
|
||||
onChange={(e) => openPage(e.target.value as number, undefined, false)}
|
||||
>
|
||||
{pages.map((page) => (
|
||||
<MenuItem key={getNextIndexFromPage(page)} value={getNextIndexFromPage(page)}>
|
||||
{page.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
type="next"
|
||||
title={t(getOptionForDirection('reader.button.next_page', 'reader.button.previous_page', direction))}
|
||||
disabled={getOptionForDirection(
|
||||
getNextIndexFromPage(currentPage) === getNextIndexFromPage(pages.slice(-1)[0]),
|
||||
!currentPage.primary.index,
|
||||
direction,
|
||||
)}
|
||||
onClick={() => openPage('next', undefined, false)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderNavBarDesktopPageNavigation = withPropsFrom(
|
||||
memo(BaseReaderNavBarDesktopPageNavigation),
|
||||
[
|
||||
userReaderStatePagesContext,
|
||||
() => ({ openPage: ReaderControls.useOpenPage() }),
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
],
|
||||
['currentPageIndex', 'pages', 'readingDirection', 'openPage'],
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
export const ReaderNavContainer = styled(Stack)({
|
||||
width: '400px',
|
||||
minWidth: '400px',
|
||||
maxWidth: '400px',
|
||||
height: '100vh',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 '@/features/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { AUTO_SCROLL_SPEED } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { coerceIn } from '@/lib/HelperFunctions.ts';
|
||||
|
||||
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>(undefined);
|
||||
|
||||
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"
|
||||
size="small"
|
||||
onBlur={(e) => {
|
||||
const value = coerceIn(+e.target.value, AUTO_SCROLL_SPEED.min, AUTO_SCROLL_SPEED.max);
|
||||
|
||||
if (value !== autoScroll.value) {
|
||||
clearTimeout(updateTimeout.current);
|
||||
setAutoScroll({ ...autoScroll, value }, true);
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const value = coerceIn(+e.target.value, AUTO_SCROLL_SPEED.min, AUTO_SCROLL_SPEED.max);
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { OffsetDoubleSpreadIcon } from '@/assets/icons/svg/OffsetDoubleSpreadIcon.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { isOffsetDoubleSpreadPagesEditable } from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
|
||||
export const ReaderNavBarDesktopOffsetDoubleSpread = ({
|
||||
readingMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
setShouldOffsetDoubleSpreads,
|
||||
}: Pick<IReaderSettings, 'readingMode' | 'shouldOffsetDoubleSpreads'> & {
|
||||
setShouldOffsetDoubleSpreads: (offset: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isOffsetDoubleSpreadPagesEditable(readingMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
sx={{ justifyContent: 'start', textTransform: 'unset' }}
|
||||
size="large"
|
||||
onClick={() => setShouldOffsetDoubleSpreads(!shouldOffsetDoubleSpreads)}
|
||||
color={shouldOffsetDoubleSpreads ? 'secondary' : 'primary'}
|
||||
variant="contained"
|
||||
startIcon={<OffsetDoubleSpreadIcon />}
|
||||
>
|
||||
{t('reader.settings.label.offset_double_spread')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FitScreenIcon from '@mui/icons-material/FitScreen';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { ValueRotationButton } from '@/features/core/components/buttons/ValueRotationButton.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
ReaderPageScaleMode,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import {
|
||||
PAGE_SCALE_VALUE_TO_DISPLAY_DATA,
|
||||
READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED,
|
||||
READER_PAGE_SCALE_MODE_VALUES,
|
||||
} from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
import { CustomButtonIcon } from '@/features/core/components/buttons/CustomButtonIcon.tsx';
|
||||
|
||||
export const ReaderNavBarDesktopPageScale = ({
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
updateSetting,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'pageScaleMode' | 'shouldStretchPage'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReaderPageScaleMode>, 'isDefaultable' | 'onDefault'> & {
|
||||
updateSetting: <Setting extends keyof Pick<IReaderSettings, 'pageScaleMode' | 'shouldStretchPage'>>(
|
||||
setting: Setting,
|
||||
value: IReaderSettings[Setting],
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }}>
|
||||
<ValueRotationButton
|
||||
{...buttonSelectInputProps}
|
||||
tooltip={t('reader.settings.page_scale.title')}
|
||||
value={pageScaleMode.isDefault ? undefined : pageScaleMode.value}
|
||||
defaultValue={pageScaleMode.isDefault ? pageScaleMode.value : undefined}
|
||||
values={READER_PAGE_SCALE_MODE_VALUES}
|
||||
setValue={(value) => updateSetting('pageScaleMode', value)}
|
||||
valueToDisplayData={PAGE_SCALE_VALUE_TO_DISPLAY_DATA}
|
||||
defaultIcon={PAGE_SCALE_VALUE_TO_DISPLAY_DATA[pageScaleMode.value].icon}
|
||||
/>
|
||||
{READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED[pageScaleMode.value] && (
|
||||
<CustomTooltip title={t('reader.settings.page_scale.stretch')}>
|
||||
<CustomButtonIcon
|
||||
onClick={() => updateSetting('shouldStretchPage', !shouldStretchPage.value)}
|
||||
sx={{ px: undefined }}
|
||||
variant="contained"
|
||||
color={shouldStretchPage.value ? 'secondary' : 'primary'}
|
||||
>
|
||||
<FitScreenIcon />
|
||||
</CustomButtonIcon>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { ReaderNavBarDesktopPageScale } from '@/features/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopPageScale.tsx';
|
||||
import { ReaderNavBarDesktopReadingMode } from '@/features/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingMode.tsx';
|
||||
import { ReaderNavBarDesktopOffsetDoubleSpread } from '@/features/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopOffsetDoubleSpread.tsx';
|
||||
import { ReaderNavBarDesktopReadingDirection } from '@/features/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopReadingDirection.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, TReaderStateMangaContext } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ReaderNavBarDesktopProps } from '@/features/reader/types/ReaderOverlay.types.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { ReaderNavBarDesktopAutoScroll } from '@/features/reader/components/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopAutoScroll.tsx';
|
||||
|
||||
const BaseReaderNavBarDesktopQuickSettings = ({
|
||||
manga,
|
||||
readingMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
readingDirection,
|
||||
autoScroll,
|
||||
openSettings,
|
||||
}: Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<ReaderNavBarDesktopProps, 'openSettings'> &
|
||||
Pick<
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
| 'readingMode'
|
||||
| 'shouldOffsetDoubleSpreads'
|
||||
| 'pageScaleMode'
|
||||
| 'shouldStretchPage'
|
||||
| 'readingDirection'
|
||||
| 'autoScroll'
|
||||
>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateSetting = ReaderService.useCreateUpdateSetting(manga ?? FALLBACK_MANGA);
|
||||
const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? FALLBACK_MANGA);
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<ReaderNavBarDesktopReadingMode
|
||||
readingMode={readingMode}
|
||||
setReadingMode={(value) => updateSetting('readingMode', value)}
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('readingMode')}
|
||||
/>
|
||||
<ReaderNavBarDesktopOffsetDoubleSpread
|
||||
readingMode={readingMode.value}
|
||||
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads.value}
|
||||
setShouldOffsetDoubleSpreads={(value) => updateSetting('shouldOffsetDoubleSpreads', value)}
|
||||
/>
|
||||
<ReaderNavBarDesktopPageScale
|
||||
pageScaleMode={pageScaleMode}
|
||||
shouldStretchPage={shouldStretchPage}
|
||||
updateSetting={updateSetting}
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('pageScaleMode')}
|
||||
/>
|
||||
<ReaderNavBarDesktopReadingDirection
|
||||
readingDirection={readingDirection}
|
||||
setReadingDirection={(value) => updateSetting('readingDirection', value)}
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('readingDirection')}
|
||||
/>
|
||||
<ReaderNavBarDesktopAutoScroll
|
||||
autoScroll={autoScroll}
|
||||
setAutoScroll={(...args) => updateSetting('autoScroll', ...args)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => openSettings()}
|
||||
size="large"
|
||||
sx={{ justifyContent: 'start', textTransform: 'none' }}
|
||||
variant="contained"
|
||||
startIcon={<SettingsIcon />}
|
||||
>
|
||||
{t('settings.title')}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderNavBarDesktopQuickSettings = withPropsFrom(
|
||||
BaseReaderNavBarDesktopQuickSettings,
|
||||
[useReaderStateMangaContext, ReaderService.useSettings],
|
||||
[
|
||||
'manga',
|
||||
'readingMode',
|
||||
'shouldOffsetDoubleSpreads',
|
||||
'pageScaleMode',
|
||||
'shouldStretchPage',
|
||||
'readingDirection',
|
||||
'autoScroll',
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { ValueRotationButton } from '@/features/core/components/buttons/ValueRotationButton.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/features/reader/types/Reader.types.ts';
|
||||
import {
|
||||
READING_DIRECTION_VALUES,
|
||||
READING_DIRECTION_VALUE_TO_DISPLAY_DATA,
|
||||
} from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
|
||||
export const ReaderNavBarDesktopReadingDirection = ({
|
||||
readingDirection,
|
||||
setReadingDirection,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'readingDirection'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
|
||||
setReadingDirection: (readingDirection: ReadingDirection) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ValueRotationButton
|
||||
{...buttonSelectInputProps}
|
||||
tooltip={t('reader.settings.label.reading_direction')}
|
||||
value={readingDirection.isDefault ? undefined : readingDirection.value}
|
||||
defaultValue={readingDirection.isDefault ? readingDirection.value : undefined}
|
||||
values={READING_DIRECTION_VALUES}
|
||||
setValue={setReadingDirection}
|
||||
valueToDisplayData={READING_DIRECTION_VALUE_TO_DISPLAY_DATA}
|
||||
defaultIcon={READING_DIRECTION_VALUE_TO_DISPLAY_DATA[readingDirection.value].icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { ValueRotationButton } from '@/features/core/components/buttons/ValueRotationButton.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/types/Reader.types.ts';
|
||||
import {
|
||||
READING_MODE_VALUE_TO_DISPLAY_DATA,
|
||||
READING_MODE_VALUES,
|
||||
} from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
|
||||
export const ReaderNavBarDesktopReadingMode = ({
|
||||
readingMode,
|
||||
setReadingMode,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'readingMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingMode>, 'isDefaultable' | 'onDefault'> & {
|
||||
setReadingMode: (mode: ReadingMode) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ValueRotationButton
|
||||
{...buttonSelectInputProps}
|
||||
tooltip={t('reader.settings.label.reading_mode')}
|
||||
value={readingMode.isDefault ? undefined : readingMode.value}
|
||||
defaultValue={readingMode.isDefault ? readingMode.value : undefined}
|
||||
values={READING_MODE_VALUES}
|
||||
setValue={setReadingMode}
|
||||
valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA}
|
||||
defaultIcon={READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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 IconButton from '@mui/material/IconButton';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import AppSettingsAltIcon from '@mui/icons-material/AppSettingsAlt';
|
||||
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import { bindDialog, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { ReaderBottomBarMobileProps } from '@/features/reader/types/ReaderOverlay.types.ts';
|
||||
import { MobileReaderProgressBar } from '@/features/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx';
|
||||
import { ReaderChapterList } from '@/features/reader/components/overlay/navigation/ReaderChapterList.tsx';
|
||||
import { ReaderBottomBarMobileQuickSettings } from '@/features/reader/components/overlay/navigation/mobile/ReaderBottomBarMobileQuickSettings.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { ReaderStateChapters, TReaderScrollbarContext } from '@/features/reader/types/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
|
||||
const BaseReaderBottomBarMobile = ({
|
||||
openSettings,
|
||||
isVisible,
|
||||
currentChapter,
|
||||
chapters,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
topOffset = 0,
|
||||
}: ReaderBottomBarMobileProps &
|
||||
Pick<ReaderStateChapters, 'currentChapter' | 'chapters'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> & { topOffset?: number }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const chapterListPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-chapter-list-dialog' });
|
||||
const quickSettingsPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-quick-settings-dialog' });
|
||||
|
||||
const [bottomBarRefHeight, setBottomBarRefHeight] = useState(0);
|
||||
const bottomBarRef = useRef<HTMLDivElement>(null);
|
||||
useResizeObserver(
|
||||
bottomBarRef,
|
||||
useCallback(() => setBottomBarRefHeight(bottomBarRef.current?.clientHeight ?? 0), [bottomBarRefHeight]),
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
chapterListPopupState.close();
|
||||
}, [currentChapter?.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
right: `${scrollbarYSize}px`,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: `calc(100% - ${topOffset}px)`,
|
||||
}}
|
||||
>
|
||||
<MobileReaderProgressBar topOffset={topOffset} bottomOffset={bottomBarRefHeight} />
|
||||
<Slide direction="up" in={isVisible}>
|
||||
<Stack
|
||||
ref={bottomBarRef}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.95),
|
||||
pb: `max(${scrollbarXSize}px, env(safe-area-inset-bottom))`,
|
||||
boxShadow: 2,
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
width: '50%',
|
||||
flexDirection: 'row',
|
||||
p: 2,
|
||||
gap: 1,
|
||||
justifyContent: 'space-evenly',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<CustomTooltip title={t('reader.button.chapter_list')}>
|
||||
<IconButton {...bindTrigger(chapterListPopupState)} color="inherit">
|
||||
<FormatListBulletedIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('reader.settings.title.quick_settings')}>
|
||||
<IconButton {...bindTrigger(quickSettingsPopupState)} color="inherit">
|
||||
<AppSettingsAltIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('settings.title')}>
|
||||
<IconButton onClick={openSettings} color="inherit">
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Slide>
|
||||
</Stack>
|
||||
{chapterListPopupState.isOpen && (
|
||||
<Dialog {...bindDialog(chapterListPopupState)} fullWidth maxWidth="md" scroll="paper">
|
||||
<DialogContent sx={{ p: 0, pb: 1 }}>
|
||||
<ReaderChapterList
|
||||
style={{
|
||||
minHeight: '15vh',
|
||||
maxHeight: '75vh',
|
||||
}}
|
||||
currentChapter={currentChapter}
|
||||
chapters={chapters}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{quickSettingsPopupState.isOpen && (
|
||||
<Dialog {...bindDialog(quickSettingsPopupState)} fullWidth maxWidth="md" scroll="paper">
|
||||
<DialogContent>
|
||||
<ReaderBottomBarMobileQuickSettings />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderBottomBarMobile = withPropsFrom(
|
||||
memo(BaseReaderBottomBarMobile),
|
||||
[useReaderStateChaptersContext, useReaderScrollbarContext],
|
||||
['currentChapter', 'chapters', 'scrollbarXSize', 'scrollbarYSize'],
|
||||
);
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReaderSettingReadingMode } from '@/features/reader/components/settings/layout/ReaderSettingReadingMode.tsx';
|
||||
import { ReaderSettingReadingDirection } from '@/features/reader/components/settings/layout/ReaderSettingReadingDirection.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { DefaultSettingFootnote } from '@/features/reader/components/settings/DefaultSettingFootnote.tsx';
|
||||
import {
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
TReaderAutoScrollContext,
|
||||
TReaderStateMangaContext,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { ReaderSettingAutoScroll } from '@/features/reader/components/settings/behaviour/ReaderSettingAutoScroll.tsx';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { useReaderAutoScrollContext } from '@/features/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||
|
||||
const BaseReaderBottomBarMobileQuickSettings = ({
|
||||
manga,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
autoScroll,
|
||||
isActive,
|
||||
toggleActive,
|
||||
}: Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'readingDirection' | 'autoScroll'> &
|
||||
Pick<TReaderAutoScrollContext, 'isActive' | 'toggleActive'>) => {
|
||||
const { t } = useTranslation();
|
||||
const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? FALLBACK_MANGA);
|
||||
|
||||
if (!manga) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<DefaultSettingFootnote />
|
||||
<ReaderSettingReadingMode
|
||||
readingMode={readingMode}
|
||||
setReadingMode={(value) => ReaderService.updateSetting(manga, 'readingMode', value)}
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('readingMode')}
|
||||
/>
|
||||
<ReaderSettingReadingDirection
|
||||
readingDirection={readingDirection}
|
||||
setReadingDirection={(value) => ReaderService.updateSetting(manga, 'readingDirection', value)}
|
||||
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, useReaderAutoScrollContext],
|
||||
['manga', 'readingMode', 'readingDirection', 'autoScroll', 'isActive', 'toggleActive'],
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { shouldForwardProp } from '@/features/core/utils/ShouldForwardProp.ts';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { getProgressBarPositionInfo } from '@/features/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
|
||||
type ProgressBarHighlightReadPagesProps = Pick<IReaderSettings, 'progressBarPosition'> & {
|
||||
currentPagesIndex: number;
|
||||
pagesLength: number;
|
||||
};
|
||||
export const ProgressBarHighlightReadPages = styled(Box, {
|
||||
shouldForwardProp: shouldForwardProp<ProgressBarHighlightReadPagesProps>([
|
||||
'currentPagesIndex',
|
||||
'pagesLength',
|
||||
'progressBarPosition',
|
||||
]),
|
||||
})<ProgressBarHighlightReadPagesProps>(({ currentPagesIndex, pagesLength, progressBarPosition }) => ({
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, {
|
||||
left: 0,
|
||||
width: `${((currentPagesIndex + 1) / pagesLength) * 100}%`,
|
||||
height: '100%',
|
||||
}),
|
||||
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isVertical, {
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: `${((currentPagesIndex + 1) / pagesLength) * 100}%`,
|
||||
}),
|
||||
}));
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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 { BoxProps } from '@mui/material/Box';
|
||||
import { ComponentProps, memo, ReactNode, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import { TypographyProps } from '@mui/material/Typography';
|
||||
import { StackProps } from '@mui/material/Stack';
|
||||
import { ReaderProgressBarProps, TReaderProgressBarContext } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { ReaderProgressBarPageNumber } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarPageNumber.tsx';
|
||||
import { ReaderProgressBarContainer } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx';
|
||||
import { ReaderProgressBarRoot } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarRoot.tsx';
|
||||
import { ReaderProgressBarSlotsContainer } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarSlotsContainer.tsx';
|
||||
import { ProgressBarHighlightReadPages } from '@/features/reader/components/overlay/progress-bar/ProgressBarHighlightReadPages.tsx';
|
||||
import { ReaderProgressBarCurrentPageSlot } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarCurrentPageSlot.tsx';
|
||||
import {
|
||||
getNextIndexFromPage,
|
||||
getPage,
|
||||
getPageForMousePos,
|
||||
getProgressBarPositionInfo,
|
||||
} from '@/features/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { getOptionForDirection as getOptionForDirectionImpl } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { ReaderProgressBarSlotsActionArea } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarSlotsActionArea.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderProgressBarContext } from '@/features/reader/contexts/ReaderProgressBarContext.tsx';
|
||||
import { ReaderProgressBarSlotWrapper } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarSlotWrapper.tsx';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
import { IReaderSettings, ReadingMode } from '@/features/reader/types/Reader.types.ts';
|
||||
|
||||
const BaseReaderProgressBar = ({
|
||||
totalPages,
|
||||
pages,
|
||||
pageLoadStates,
|
||||
currentPageIndex,
|
||||
slotProps,
|
||||
slots,
|
||||
createProgressBarSlot,
|
||||
progressBarPosition,
|
||||
readingMode,
|
||||
isDragging,
|
||||
setIsDragging,
|
||||
openPage,
|
||||
direction,
|
||||
fullSegmentClicks,
|
||||
}: ReaderProgressBarProps &
|
||||
Pick<IReaderSettings, 'readingMode'> &
|
||||
Pick<TReaderProgressBarContext, 'isDragging' | 'setIsDragging'> &
|
||||
Pick<ComponentProps<typeof ReaderProgressBarSlotWrapper>, 'createProgressBarSlot'> & {
|
||||
slotProps?: {
|
||||
container?: StackProps;
|
||||
progressBarRoot?: StackProps;
|
||||
progressBarSlotsActionArea?: StackProps;
|
||||
progressBarSlotsContainer?: StackProps;
|
||||
progressBarSlot?: BoxProps;
|
||||
progressBarReadPages?: BoxProps;
|
||||
progressBarCurrentPageSlot?: BoxProps;
|
||||
progressBarPageTexts?: {
|
||||
base?: TypographyProps;
|
||||
current?: TypographyProps;
|
||||
total?: TypographyProps;
|
||||
};
|
||||
};
|
||||
slots?: {
|
||||
progressBarCurrentPage?: ReactNode;
|
||||
};
|
||||
openPage: ReturnType<typeof ReaderControls.useOpenPage>;
|
||||
direction: ReturnType<typeof ReaderService.useGetThemeDirection>;
|
||||
fullSegmentClicks: boolean;
|
||||
}) => {
|
||||
const progressBarRef = useRef<HTMLDivElement | null>(null);
|
||||
const draggingDetectionTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
|
||||
const [totalPagesTextWidth, setTotalPagesTextWidth] = useState(0);
|
||||
const totalPagesTextRef = useRef<HTMLSpanElement | null>(null);
|
||||
useResizeObserver(
|
||||
totalPagesTextRef,
|
||||
useCallback(() => {
|
||||
const element = totalPagesTextRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { paddingLeft, paddingRight } = getComputedStyle(element);
|
||||
|
||||
const PAGE_NAME_SEPARATOR_LENGTH = 6; // <primaryPage>-<secondaryPage>
|
||||
const widthOfTotalPageText = element.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
|
||||
const newWidth =
|
||||
readingMode === ReadingMode.DOUBLE_PAGE
|
||||
? widthOfTotalPageText * 2 + PAGE_NAME_SEPARATOR_LENGTH
|
||||
: widthOfTotalPageText;
|
||||
|
||||
setTotalPagesTextWidth(newWidth);
|
||||
}, [readingMode]),
|
||||
);
|
||||
|
||||
const currentPagesIndex = useMemo(() => getPage(currentPageIndex, pages).pagesIndex, [currentPageIndex, pages]);
|
||||
|
||||
const isHorizontalPosition = getProgressBarPositionInfo(progressBarPosition).isHorizontal;
|
||||
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
|
||||
const getOptionForDirection = useCallback(
|
||||
<T,>(...args: Parameters<typeof getOptionForDirectionImpl<T>>) =>
|
||||
getOptionForDirectionImpl(args[0], args[1], direction),
|
||||
[direction],
|
||||
);
|
||||
|
||||
ReaderControls.useHandleProgressDragging(
|
||||
openPage,
|
||||
progressBarRef,
|
||||
isDragging,
|
||||
currentPage,
|
||||
pages,
|
||||
progressBarPosition,
|
||||
getOptionForDirection,
|
||||
fullSegmentClicks,
|
||||
);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent | React.TouchEvent) => {
|
||||
if (!progressBarRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isTouchEvent = 'touches' in e;
|
||||
|
||||
openPage(
|
||||
getNextIndexFromPage(
|
||||
getPageForMousePos(
|
||||
isTouchEvent ? e.touches[0] : e,
|
||||
progressBarRef.current,
|
||||
pages,
|
||||
isHorizontalPosition,
|
||||
fullSegmentClicks,
|
||||
getOptionForDirection,
|
||||
),
|
||||
),
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
clearTimeout(draggingDetectionTimeout.current);
|
||||
draggingDetectionTimeout.current = setTimeout(() => setIsDragging(true), 250);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
clearTimeout(draggingDetectionTimeout.current);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReaderProgressBarContainer {...slotProps?.container} progressBarPosition={progressBarPosition}>
|
||||
<ReaderProgressBarRoot {...slotProps?.progressBarRoot}>
|
||||
<ReaderProgressBarPageNumber
|
||||
{...slotProps?.progressBarPageTexts?.base}
|
||||
{...slotProps?.progressBarPageTexts?.current}
|
||||
sx={[
|
||||
{ width: `${totalPagesTextWidth}px` },
|
||||
...(Array.isArray(slotProps?.progressBarPageTexts?.base?.sx)
|
||||
? (slotProps?.progressBarPageTexts?.base?.sx ?? [])
|
||||
: [slotProps?.progressBarPageTexts?.base?.sx]),
|
||||
...(Array.isArray(slotProps?.progressBarPageTexts?.current?.sx)
|
||||
? (slotProps?.progressBarPageTexts?.current?.sx ?? [])
|
||||
: [slotProps?.progressBarPageTexts?.current?.sx]),
|
||||
]}
|
||||
onClick={() => openPage('previous', 'ltr', false)}
|
||||
>
|
||||
{currentPage.name}
|
||||
</ReaderProgressBarPageNumber>
|
||||
<ClickAwayListener onClickAway={() => setIsDragging(false)}>
|
||||
<ReaderProgressBarSlotsActionArea
|
||||
{...slotProps?.progressBarSlotsActionArea}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
onTouchStart={handleMouseDown}
|
||||
onTouchEnd={handleMouseUp}
|
||||
>
|
||||
<ReaderProgressBarSlotsContainer ref={progressBarRef} {...slotProps?.progressBarSlotsContainer}>
|
||||
{pages.map((page, pagesIndex) => (
|
||||
<ReaderProgressBarSlotWrapper
|
||||
{...slotProps?.progressBarSlot}
|
||||
key={page.primary.index}
|
||||
page={page}
|
||||
pagesIndex={pagesIndex}
|
||||
isCurrentPage={pagesIndex === currentPagesIndex}
|
||||
isLeadingPage={pagesIndex < currentPagesIndex}
|
||||
isTrailingPage={pagesIndex > currentPagesIndex}
|
||||
totalPages={pages.length}
|
||||
primaryPageLoadState={pageLoadStates[page.primary.index].loaded}
|
||||
secondaryPageLoadState={
|
||||
page.secondary ? pageLoadStates[page.secondary.index].loaded : undefined
|
||||
}
|
||||
createProgressBarSlot={createProgressBarSlot}
|
||||
showDraggingStyle={pagesIndex === currentPagesIndex && isDragging}
|
||||
/>
|
||||
))}
|
||||
<ProgressBarHighlightReadPages
|
||||
{...slotProps?.progressBarReadPages}
|
||||
currentPagesIndex={currentPage.pagesIndex}
|
||||
pagesLength={pages.length}
|
||||
progressBarPosition={progressBarPosition}
|
||||
/>
|
||||
</ReaderProgressBarSlotsContainer>
|
||||
<ReaderProgressBarCurrentPageSlot
|
||||
boxProps={slotProps?.progressBarCurrentPageSlot}
|
||||
pageName={currentPage.name}
|
||||
currentPagesIndex={currentPage.pagesIndex}
|
||||
pagesLength={pages.length}
|
||||
isDragging={isDragging}
|
||||
progressBarPosition={progressBarPosition}
|
||||
>
|
||||
{slots?.progressBarCurrentPage}
|
||||
</ReaderProgressBarCurrentPageSlot>
|
||||
</ReaderProgressBarSlotsActionArea>
|
||||
</ClickAwayListener>
|
||||
<ReaderProgressBarPageNumber
|
||||
ref={totalPagesTextRef}
|
||||
{...slotProps?.progressBarPageTexts?.base}
|
||||
{...slotProps?.progressBarPageTexts?.total}
|
||||
sx={[
|
||||
{
|
||||
minWidth: 'unset',
|
||||
},
|
||||
...(Array.isArray(slotProps?.progressBarPageTexts?.base?.sx)
|
||||
? (slotProps?.progressBarPageTexts?.base?.sx ?? [])
|
||||
: [slotProps?.progressBarPageTexts?.base?.sx]),
|
||||
...(Array.isArray(slotProps?.progressBarPageTexts?.total?.sx)
|
||||
? (slotProps?.progressBarPageTexts?.total?.sx ?? [])
|
||||
: [slotProps?.progressBarPageTexts?.total?.sx]),
|
||||
]}
|
||||
onClick={() => openPage('next', 'ltr', false)}
|
||||
>
|
||||
{totalPages}
|
||||
</ReaderProgressBarPageNumber>
|
||||
</ReaderProgressBarRoot>
|
||||
</ReaderProgressBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderProgressBar = withPropsFrom(
|
||||
memo(BaseReaderProgressBar),
|
||||
[
|
||||
useReaderProgressBarContext,
|
||||
() => ({ openPage: ReaderControls.useOpenPage() }),
|
||||
userReaderStatePagesContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
],
|
||||
[
|
||||
'isDragging',
|
||||
'setIsDragging',
|
||||
'openPage',
|
||||
'pages',
|
||||
'pageLoadStates',
|
||||
'totalPages',
|
||||
'currentPageIndex',
|
||||
'readingMode',
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { shouldForwardProp } from '@/features/core/utils/ShouldForwardProp.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { getProgressBarPositionInfo } from '@/features/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
|
||||
type ReaderProgressBarContainerProps = Pick<IReaderSettings, 'progressBarPosition'>;
|
||||
export const ReaderProgressBarContainer = styled(Stack, {
|
||||
shouldForwardProp: shouldForwardProp<ReaderProgressBarContainerProps>(['progressBarPosition']),
|
||||
})<ReaderProgressBarContainerProps>(({ theme, progressBarPosition }) => ({
|
||||
position: 'fixed',
|
||||
pointerEvents: 'all',
|
||||
touchAction: 'none',
|
||||
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, {
|
||||
justifyContent: 'flex-end',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}),
|
||||
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isVertical, {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}),
|
||||
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isLeft, {
|
||||
alignItems: theme.direction === 'ltr' ? 'flex-start' : 'flex-end',
|
||||
}),
|
||||
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isRight, {
|
||||
alignItems: theme.direction === 'ltr' ? 'flex-end' : 'flex-start',
|
||||
}),
|
||||
}));
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import { ReactNode } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { CurrentPageSlotProps } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { getProgressBarPositionInfo } from '@/features/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderProgressBarCurrentPageSlot = ({
|
||||
pageName,
|
||||
currentPagesIndex,
|
||||
pagesLength,
|
||||
isDragging,
|
||||
boxProps,
|
||||
children,
|
||||
progressBarPosition,
|
||||
}: CurrentPageSlotProps & { children?: ReactNode }) => (
|
||||
<CustomTooltip
|
||||
title={pageName}
|
||||
slotProps={{
|
||||
tooltip: { sx: { backgroundColor: 'primary.main', color: 'primary.contrastText' } },
|
||||
}}
|
||||
placement={READER_PROGRESS_BAR_POSITION_TO_PLACEMENT[progressBarPosition]}
|
||||
>
|
||||
<Box
|
||||
{...boxProps}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isHorizontal, {
|
||||
left: `${(Math.max(0, currentPagesIndex - 1) / (pagesLength - 1)) * 100}%`,
|
||||
width: `calc(100% / ${pagesLength - 1})`,
|
||||
height: '100%',
|
||||
}),
|
||||
...applyStyles(getProgressBarPositionInfo(progressBarPosition).isVertical, {
|
||||
top: `${(Math.max(0, currentPagesIndex - 1) / (pagesLength - 1)) * 100}%`,
|
||||
width: '100%',
|
||||
height: `calc(100% / ${pagesLength - 1})`,
|
||||
}),
|
||||
...boxProps?.sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</CustomTooltip>
|
||||
);
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { forwardRef, useMemo } from 'react';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import Box, { BoxProps } from '@mui/material/Box';
|
||||
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
|
||||
import { getTheme } from '@/features/theme/services/AppThemes.ts';
|
||||
import { createTheme } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { DIRECTION_TO_CACHE } from '@/features/theme/ThemeDirectionCache.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
|
||||
const BaseReaderProgressBarDirectionWrapper = forwardRef<
|
||||
HTMLElement,
|
||||
BoxProps & {
|
||||
direction: ReturnType<typeof ReaderService.useGetThemeDirection>;
|
||||
}
|
||||
>(({ direction, ...boxProps }, ref) => {
|
||||
const {
|
||||
settings: { customThemes, appTheme, themeMode, shouldUsePureBlackMode },
|
||||
} = useMetadataServerSettings();
|
||||
|
||||
const readerTheme = useMemo(
|
||||
() => createTheme(themeMode, getTheme(appTheme, customThemes), shouldUsePureBlackMode, direction),
|
||||
[themeMode, appTheme, customThemes, shouldUsePureBlackMode, direction],
|
||||
);
|
||||
|
||||
return (
|
||||
<CacheProvider value={DIRECTION_TO_CACHE[direction]}>
|
||||
<ThemeProvider theme={readerTheme}>
|
||||
<Box {...boxProps} ref={ref} dir={direction} />
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
);
|
||||
});
|
||||
|
||||
export const ReaderProgressBarDirectionWrapper = withPropsFrom(
|
||||
BaseReaderProgressBarDirectionWrapper,
|
||||
[() => ({ direction: ReaderService.useGetThemeDirection() })],
|
||||
['direction'],
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 { styled } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export const ReaderProgressBarPageNumber = styled(Typography)({
|
||||
boxSizing: 'content-box',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
textAlign: 'center',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { styled } from '@mui/material/styles';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
export const ReaderProgressBarRoot = styled(Stack)(({ theme }) => ({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(0.5),
|
||||
}));
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import { memo, ReactNode } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { ReaderProgressBarSlotProps } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
|
||||
import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderProgressBarSlot = memo(
|
||||
({ pageName, progressBarPosition, slotProps, children }: ReaderProgressBarSlotProps & { children?: ReactNode }) => (
|
||||
<CustomTooltip
|
||||
{...slotProps?.tooltip}
|
||||
title={pageName}
|
||||
placement={READER_PROGRESS_BAR_POSITION_TO_PLACEMENT[progressBarPosition]}
|
||||
>
|
||||
<Box {...slotProps?.box} sx={{ width: '100%', height: '100%', ...slotProps?.box?.sx }}>
|
||||
{children}
|
||||
</Box>
|
||||
</CustomTooltip>
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 Box, { BoxProps } from '@mui/material/Box';
|
||||
import { memo, ReactNode, useMemo } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { ReaderProgressBarProps } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { shouldForwardProp } from '@/features/core/utils/ShouldForwardProp.ts';
|
||||
|
||||
type StyledWrapperProps = {
|
||||
isFirstPage: boolean;
|
||||
isLastPage: boolean;
|
||||
};
|
||||
const StyledWrapper = memo(
|
||||
styled(Box, {
|
||||
shouldForwardProp: shouldForwardProp<StyledWrapperProps>(['isFirstPage', 'isLastPage']),
|
||||
})<BoxProps & StyledWrapperProps>(({ isFirstPage, isLastPage }) => ({
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
cursor: 'pointer',
|
||||
borderLeftWidth: isFirstPage ? 0 : undefined,
|
||||
borderRightWidth: isLastPage ? 0 : undefined,
|
||||
})),
|
||||
);
|
||||
|
||||
export const ReaderProgressBarSlotWrapper = memo(
|
||||
({
|
||||
page,
|
||||
pagesIndex,
|
||||
isCurrentPage,
|
||||
isLeadingPage,
|
||||
isTrailingPage,
|
||||
totalPages,
|
||||
showDraggingStyle,
|
||||
primaryPageLoadState,
|
||||
secondaryPageLoadState,
|
||||
createProgressBarSlot,
|
||||
...boxProps
|
||||
}: {
|
||||
page: ReaderProgressBarProps['pages'][number];
|
||||
pagesIndex: number;
|
||||
isCurrentPage: boolean;
|
||||
isLeadingPage: boolean;
|
||||
isTrailingPage: boolean;
|
||||
totalPages: number;
|
||||
showDraggingStyle: boolean;
|
||||
primaryPageLoadState: ReaderProgressBarProps['pageLoadStates'][number]['loaded'];
|
||||
secondaryPageLoadState: ReaderProgressBarProps['pageLoadStates'][number]['loaded'] | undefined;
|
||||
createProgressBarSlot: (
|
||||
page: ReaderProgressBarProps['pages'][number],
|
||||
pagesIndex: number,
|
||||
primaryPageLoadState: ReaderProgressBarProps['pageLoadStates'][number]['loaded'],
|
||||
secondaryPageLoadState: ReaderProgressBarProps['pageLoadStates'][number]['loaded'] | undefined,
|
||||
isCurrentPage: boolean,
|
||||
isLeadingPage: boolean,
|
||||
isTrailingPage: boolean,
|
||||
totalPages: number,
|
||||
handleDragging: boolean,
|
||||
) => ReactNode;
|
||||
} & BoxProps) => {
|
||||
const slot = useMemo(
|
||||
() =>
|
||||
createProgressBarSlot(
|
||||
page,
|
||||
pagesIndex,
|
||||
primaryPageLoadState,
|
||||
secondaryPageLoadState,
|
||||
isCurrentPage,
|
||||
isLeadingPage,
|
||||
isTrailingPage,
|
||||
totalPages,
|
||||
showDraggingStyle,
|
||||
),
|
||||
[
|
||||
createProgressBarSlot,
|
||||
page,
|
||||
pagesIndex,
|
||||
primaryPageLoadState,
|
||||
secondaryPageLoadState,
|
||||
isCurrentPage,
|
||||
isLeadingPage,
|
||||
isTrailingPage,
|
||||
totalPages,
|
||||
showDraggingStyle,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper {...boxProps} isFirstPage={pagesIndex === 0} isLastPage={pagesIndex === totalPages - 1}>
|
||||
{slot}
|
||||
</StyledWrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
export const ReaderProgressBarSlotsActionArea = styled(Stack)({
|
||||
position: 'relative',
|
||||
flexDirection: 'row',
|
||||
flexGrow: 1,
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 { styled } from '@mui/material/styles';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
export const ReaderProgressBarSlotsContainer = styled(Stack)({
|
||||
flexDirection: 'row',
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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 { alpha, darken, lighten, useTheme } from '@mui/material/styles';
|
||||
import Box from '@mui/material/Box';
|
||||
import { memo } from 'react';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { ReaderProgressBarSlot } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
|
||||
export const ReaderProgressBarSlotDesktop = memo(
|
||||
({
|
||||
pageName,
|
||||
primaryPageLoadState,
|
||||
secondaryPageLoadState,
|
||||
progressBarPosition,
|
||||
isCurrentPage,
|
||||
isLeadingPage,
|
||||
showDraggingStyle,
|
||||
}: Pick<IReaderSettings, 'progressBarPosition'> & {
|
||||
pageName: string;
|
||||
primaryPageLoadState: boolean;
|
||||
secondaryPageLoadState?: boolean;
|
||||
isCurrentPage: boolean;
|
||||
isLeadingPage: boolean;
|
||||
showDraggingStyle: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<ReaderProgressBarSlot
|
||||
pageName={pageName}
|
||||
progressBarPosition={progressBarPosition}
|
||||
slotProps={{
|
||||
box: {
|
||||
sx: {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: darken(theme.palette.background.paper, 0.2),
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundColor: lighten(theme.palette.background.paper, 0.1),
|
||||
}),
|
||||
...applyStyles(
|
||||
primaryPageLoadState && (secondaryPageLoadState == null || secondaryPageLoadState),
|
||||
{
|
||||
backgroundColor: darken(theme.palette.background.paper, 0.35),
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundColor: lighten(theme.palette.background.paper, 0.25),
|
||||
}),
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
slotProps: isCurrentPage
|
||||
? {
|
||||
tooltip: {
|
||||
sx: {
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
...applyStyles(isLeadingPage, {
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.5),
|
||||
}),
|
||||
...applyStyles(isCurrentPage, {
|
||||
cursor: showDraggingStyle ? 'grabbing' : 'grab',
|
||||
pointer: 'grabbing',
|
||||
borderRadius: 2,
|
||||
backgroundColor: 'primary.dark',
|
||||
...theme.applyStyles('dark', {
|
||||
backgroundColor: 'primary.light',
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</ReaderProgressBarSlot>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import { ComponentProps, memo } from 'react';
|
||||
import { ProgressBarPosition } from '@/features/reader/types/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { ReaderProgressBarSlot } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarSlot';
|
||||
|
||||
const SLOT_SX_PROP: NonNullable<NonNullable<ComponentProps<typeof ReaderProgressBarSlot>['slotProps']>['box']>['sx'] = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'end',
|
||||
position: 'relative',
|
||||
backgroundColor: 'background.default',
|
||||
};
|
||||
|
||||
const POINT_SIZE = '3px';
|
||||
const ProgressBarPagePoint = memo(
|
||||
({
|
||||
isTrailingPage,
|
||||
pagesIndex,
|
||||
totalPages,
|
||||
isVertical,
|
||||
isHorizontal,
|
||||
}: {
|
||||
isTrailingPage: boolean;
|
||||
pagesIndex: number;
|
||||
totalPages: number;
|
||||
isVertical: boolean;
|
||||
isHorizontal: boolean;
|
||||
}) => {
|
||||
const position = `${(pagesIndex / (totalPages - 1)) * 100}%`;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
...applyStyles(isVertical, {
|
||||
top: position,
|
||||
left: '50%',
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
top: '50%',
|
||||
left: position,
|
||||
}),
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: POINT_SIZE,
|
||||
height: POINT_SIZE,
|
||||
borderRadius: 100,
|
||||
backgroundColor: 'background.paper',
|
||||
...applyStyles(isTrailingPage, {
|
||||
backgroundColor: 'primary.main',
|
||||
}),
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ReaderProgressBarSlotMobile = memo(
|
||||
({
|
||||
pageName,
|
||||
isTrailingPage,
|
||||
isCurrentPage,
|
||||
pagesIndex,
|
||||
totalPages,
|
||||
isVertical,
|
||||
isHorizontal,
|
||||
}: {
|
||||
pageName: string;
|
||||
isTrailingPage: boolean;
|
||||
isCurrentPage: boolean;
|
||||
pagesIndex: number;
|
||||
totalPages: number;
|
||||
isVertical: boolean;
|
||||
isHorizontal: boolean;
|
||||
}) => (
|
||||
<ReaderProgressBarSlot
|
||||
pageName={pageName}
|
||||
progressBarPosition={ProgressBarPosition.BOTTOM}
|
||||
slotProps={{ box: { sx: SLOT_SX_PROP } }}
|
||||
>
|
||||
{!isCurrentPage && (
|
||||
<ProgressBarPagePoint
|
||||
isTrailingPage={isTrailingPage}
|
||||
pagesIndex={pagesIndex}
|
||||
totalPages={totalPages}
|
||||
isVertical={isVertical}
|
||||
isHorizontal={isHorizontal}
|
||||
/>
|
||||
)}
|
||||
</ReaderProgressBarSlot>
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* 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 IconButton from '@mui/material/IconButton';
|
||||
import SkipPreviousIcon from '@mui/icons-material/SkipPrevious';
|
||||
import SkipNextIcon from '@mui/icons-material/SkipNext';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import Box from '@mui/material/Box';
|
||||
import { ComponentProps, memo, useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||
import { ReaderProgressBar } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBar.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { getPage, getProgressBarPositionInfo } from '@/features/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ProgressBarPosition,
|
||||
ReaderStateChapters,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { ReaderProgressBarDirectionWrapper } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx';
|
||||
import { useReaderProgressBarContext } from '@/features/reader/contexts/ReaderProgressBarContext.tsx';
|
||||
import { useReaderOverlayContext } from '@/features/reader/contexts/ReaderOverlayContext.tsx';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { TReaderOverlayContext } from '@/features/reader/types/ReaderOverlay.types.ts';
|
||||
import { ReaderProgressBarProps, TReaderProgressBarContext } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { ReaderProgressBarSlotMobile } from '@/features/reader/components/overlay/progress-bar/mobile/ReaderProgressBarSlotMobile.tsx';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
import { getProgressBarPosition } from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
|
||||
const PROGRESS_BAR_POSITION_TO_SLIDE_DIRECTION: Record<ProgressBarPosition, SlideProps['direction']> = {
|
||||
[ProgressBarPosition.BOTTOM]: 'up',
|
||||
[ProgressBarPosition.LEFT]: 'right',
|
||||
[ProgressBarPosition.RIGHT]: 'left',
|
||||
// should never get accessed
|
||||
[ProgressBarPosition.AUTO]: 'left',
|
||||
};
|
||||
|
||||
const BaseMobileReaderProgressBar = ({
|
||||
previousChapter,
|
||||
nextChapter,
|
||||
isVisible,
|
||||
setIsMaximized,
|
||||
isDragging,
|
||||
currentPageIndex,
|
||||
pages,
|
||||
direction: readerDirection,
|
||||
progressBarPosition,
|
||||
progressBarPositionAutoVertical,
|
||||
topOffset = 0,
|
||||
bottomOffset = 0,
|
||||
scrollbarXSize,
|
||||
}: Pick<ReaderStateChapters, 'previousChapter' | 'nextChapter'> &
|
||||
Pick<TReaderOverlayContext, 'isVisible'> &
|
||||
Pick<TReaderProgressBarContext, 'setIsMaximized' | 'isDragging'> &
|
||||
Pick<ReaderProgressBarProps, 'currentPageIndex' | 'pages'> &
|
||||
Pick<IReaderSettings, 'progressBarPosition' | 'progressBarPositionAutoVertical'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarXSize'> & {
|
||||
direction: ReturnType<typeof ReaderService.useGetThemeDirection>;
|
||||
topOffset?: number;
|
||||
bottomOffset?: number;
|
||||
}) => {
|
||||
const openChapter = ReaderControls.useOpenChapter();
|
||||
|
||||
const [, setRefreshProgressBarPosition] = useState({});
|
||||
useResizeObserver(
|
||||
window.document.documentElement,
|
||||
useCallback(() => setRefreshProgressBarPosition({}), []),
|
||||
);
|
||||
|
||||
const finalProgressBarPosition = getProgressBarPosition(
|
||||
progressBarPosition,
|
||||
progressBarPositionAutoVertical,
|
||||
// scrollbar x size is already included in the top/bottom offset due to the progress bar being placed in the reader mobile bottom bar
|
||||
topOffset + bottomOffset,
|
||||
scrollbarXSize,
|
||||
);
|
||||
|
||||
const { isLeft, isRight, isVertical, isHorizontal } = getProgressBarPositionInfo(finalProgressBarPosition);
|
||||
const finalReaderDirection = isHorizontal ? readerDirection : 'ltr';
|
||||
const currentPagesIndex = useMemo(() => getPage(currentPageIndex, pages).pagesIndex, [currentPageIndex, pages]);
|
||||
|
||||
const progressBarSlotProps: ComponentProps<typeof ReaderProgressBar>['slotProps'] = useMemo(
|
||||
() => ({
|
||||
container: {
|
||||
sx: {
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
justifyItems: 'center',
|
||||
alignItems: 'stretch',
|
||||
backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.85),
|
||||
borderRadius: 100,
|
||||
boxShadow: 2,
|
||||
},
|
||||
},
|
||||
progressBarRoot: {
|
||||
sx: {
|
||||
flexGrow: 1,
|
||||
gap: 0,
|
||||
...applyStyles(isVertical, {
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
alignItems: 'stretch',
|
||||
}),
|
||||
},
|
||||
},
|
||||
progressBarSlotsActionArea: {
|
||||
sx: {
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
...applyStyles(isVertical, {
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
px: 2,
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
height: '100%',
|
||||
py: 2,
|
||||
}),
|
||||
},
|
||||
},
|
||||
progressBarSlotsContainer: {
|
||||
sx: {
|
||||
borderRadius: 100,
|
||||
backgroundColor: 'background.default',
|
||||
...applyStyles(isVertical, {
|
||||
flexDirection: 'column',
|
||||
py: 1,
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
px: 1,
|
||||
}),
|
||||
},
|
||||
},
|
||||
progressBarSlot: {
|
||||
sx: {
|
||||
...applyStyles(isVertical, {
|
||||
width: '20px',
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
height: '20px',
|
||||
}),
|
||||
},
|
||||
},
|
||||
progressBarCurrentPageSlot: {
|
||||
sx: {
|
||||
display: 'flex',
|
||||
zIndex: 1,
|
||||
cursor: 'inherit',
|
||||
...applyStyles(isVertical, {
|
||||
justifyContent: 'center',
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
alignItems: 'center',
|
||||
}),
|
||||
},
|
||||
},
|
||||
progressBarPageTexts: {
|
||||
base: {
|
||||
sx: {
|
||||
...applyStyles(isVertical, {
|
||||
py: 1,
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
px: 1,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[isVertical, isHorizontal],
|
||||
);
|
||||
|
||||
const progressBarCurrentPage = useMemo(
|
||||
() => ({
|
||||
progressBarCurrentPage: (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
...applyStyles(isVertical, {
|
||||
top: 'calc(100% - 6px)',
|
||||
...applyStyles(currentPagesIndex === 0, {
|
||||
top: '0',
|
||||
}),
|
||||
width: '75%',
|
||||
height: '6px',
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
left: 'calc(100% - 0px)',
|
||||
...applyStyles(currentPagesIndex === 0, {
|
||||
left: '0',
|
||||
}),
|
||||
width: '6px',
|
||||
height: '75%',
|
||||
}),
|
||||
backgroundColor: 'primary.main',
|
||||
borderRadius: 100,
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[currentPagesIndex, pages.length, isDragging, isVertical, isHorizontal],
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setIsMaximized(isVisible);
|
||||
|
||||
return () => setIsMaximized(false);
|
||||
}, [isVisible]);
|
||||
|
||||
return (
|
||||
<Slide direction={PROGRESS_BAR_POSITION_TO_SLIDE_DIRECTION[finalProgressBarPosition]} in={isVisible}>
|
||||
<ReaderProgressBarDirectionWrapper
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'end',
|
||||
height: '100%',
|
||||
pointerEvents: 'none',
|
||||
...applyStyles(isLeft, {
|
||||
justifyContent: readerDirection === 'ltr' ? 'start' : 'end',
|
||||
}),
|
||||
...applyStyles(isRight, {
|
||||
justifyContent: readerDirection === 'ltr' ? 'end' : 'start',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
p: 2,
|
||||
gap: 1,
|
||||
pointerEvents: 'all',
|
||||
alignItems: 'center',
|
||||
...applyStyles(isVertical, {
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => openChapter('previous')}
|
||||
disabled={!previousChapter}
|
||||
sx={{
|
||||
backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.85),
|
||||
boxShadow: 2,
|
||||
}}
|
||||
>
|
||||
{getOptionForDirection(
|
||||
<SkipPreviousIcon
|
||||
sx={{
|
||||
...applyStyles(isVertical, {
|
||||
transform: 'rotate(90deg)',
|
||||
}),
|
||||
}}
|
||||
/>,
|
||||
<SkipNextIcon
|
||||
sx={{
|
||||
...applyStyles(isVertical, {
|
||||
transform: 'rotate(90deg)',
|
||||
}),
|
||||
}}
|
||||
/>,
|
||||
finalReaderDirection,
|
||||
)}
|
||||
</IconButton>
|
||||
<ReaderProgressBar
|
||||
direction={finalReaderDirection}
|
||||
progressBarPosition={finalProgressBarPosition}
|
||||
fullSegmentClicks={false}
|
||||
createProgressBarSlot={useCallback(
|
||||
(
|
||||
page,
|
||||
pagesIndex,
|
||||
_primaryPageLoadState,
|
||||
_secondaryPageLoadState,
|
||||
isCurrentPage,
|
||||
_isLeadingPage,
|
||||
isTrailingPage,
|
||||
totalPages,
|
||||
) => (
|
||||
<ReaderProgressBarSlotMobile
|
||||
pageName={page.name}
|
||||
isTrailingPage={isTrailingPage}
|
||||
isCurrentPage={isCurrentPage}
|
||||
pagesIndex={pagesIndex}
|
||||
totalPages={totalPages}
|
||||
isVertical={isVertical}
|
||||
isHorizontal={isHorizontal}
|
||||
/>
|
||||
),
|
||||
[isVertical, isHorizontal],
|
||||
)}
|
||||
slotProps={{
|
||||
...progressBarSlotProps,
|
||||
progressBarReadPages: {
|
||||
sx: {
|
||||
backgroundColor: 'primary.main',
|
||||
...applyStyles(isVertical, {
|
||||
width: '20px',
|
||||
height: `${(Math.max(0, getPage(currentPageIndex, pages).pagesIndex) / (pages.length - 1)) * 100}%`,
|
||||
borderRadius: '400px 400px 0 0',
|
||||
}),
|
||||
...applyStyles(isHorizontal, {
|
||||
width: `${(Math.max(0, getPage(currentPageIndex, pages).pagesIndex) / (pages.length - 1)) * 100}%`,
|
||||
height: '20px',
|
||||
borderRadius: '400px 0 0 400px',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}}
|
||||
slots={progressBarCurrentPage}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => openChapter('next')}
|
||||
disabled={!nextChapter}
|
||||
sx={{ backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.85), boxShadow: 2 }}
|
||||
>
|
||||
{getOptionForDirection(
|
||||
<SkipNextIcon
|
||||
sx={{
|
||||
...applyStyles(isVertical, {
|
||||
transform: 'rotate(90deg)',
|
||||
}),
|
||||
}}
|
||||
/>,
|
||||
<SkipPreviousIcon
|
||||
sx={{
|
||||
...applyStyles(isVertical, {
|
||||
transform: 'rotate(90deg)',
|
||||
}),
|
||||
}}
|
||||
/>,
|
||||
finalReaderDirection,
|
||||
)}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</ReaderProgressBarDirectionWrapper>
|
||||
</Slide>
|
||||
);
|
||||
};
|
||||
|
||||
export const MobileReaderProgressBar = withPropsFrom(
|
||||
memo(BaseMobileReaderProgressBar),
|
||||
[
|
||||
useReaderStateChaptersContext,
|
||||
useReaderOverlayContext,
|
||||
useReaderProgressBarContext,
|
||||
userReaderStatePagesContext,
|
||||
() => ({ direction: ReaderService.useGetThemeDirection() }),
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
useReaderScrollbarContext,
|
||||
],
|
||||
[
|
||||
'previousChapter',
|
||||
'nextChapter',
|
||||
'isVisible',
|
||||
'setIsMaximized',
|
||||
'isDragging',
|
||||
'currentPageIndex',
|
||||
'pages',
|
||||
'direction',
|
||||
'progressBarPosition',
|
||||
'progressBarPositionAutoVertical',
|
||||
'scrollbarXSize',
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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 { useTheme } from '@mui/material/styles';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { ReaderProgressBar } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBar.tsx';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { IReaderSettings, ProgressBarType, TReaderScrollbarContext } from '@/features/reader/types/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { getProgressBarPositionInfo } from '@/features/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
import { ReaderProgressBarDirectionWrapper } from '@/features/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx';
|
||||
import { ReaderProgressBarProps, TReaderProgressBarContext } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { useReaderProgressBarContext } from '@/features/reader/contexts/ReaderProgressBarContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { ReaderProgressBarSlotDesktop } from '@/features/reader/components/overlay/progress-bar/desktop/ReaderProgressBarSlotDesktop.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
import { getProgressBarPosition } from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
|
||||
const BaseStandardReaderProgressBar = ({
|
||||
readerNavBarWidth,
|
||||
isMaximized,
|
||||
setIsMaximized,
|
||||
isDragging,
|
||||
progressBarType,
|
||||
progressBarSize,
|
||||
progressBarPosition,
|
||||
progressBarPositionAutoVertical,
|
||||
readerDirection,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
totalPages,
|
||||
}: Pick<NavbarContextType, 'readerNavBarWidth'> &
|
||||
Pick<TReaderProgressBarContext, 'isMaximized' | 'setIsMaximized' | 'isDragging'> &
|
||||
Pick<
|
||||
IReaderSettings,
|
||||
'progressBarType' | 'progressBarSize' | 'progressBarPosition' | 'progressBarPositionAutoVertical'
|
||||
> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> &
|
||||
Pick<ReaderProgressBarProps, 'totalPages'> & {
|
||||
readerDirection: ReturnType<typeof ReaderService.useGetThemeDirection>;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [, setRefreshProgressBarPosition] = useState({});
|
||||
useResizeObserver(
|
||||
window.document.documentElement,
|
||||
useCallback(() => setRefreshProgressBarPosition({}), []),
|
||||
);
|
||||
|
||||
const finalProgressBarPosition = getProgressBarPosition(
|
||||
progressBarPosition,
|
||||
progressBarPositionAutoVertical,
|
||||
scrollbarYSize,
|
||||
readerNavBarWidth + scrollbarXSize,
|
||||
);
|
||||
const { isBottom, isLeft, isRight, isVertical, isHorizontal } =
|
||||
getProgressBarPositionInfo(finalProgressBarPosition);
|
||||
|
||||
const arePagesLoaded = !!totalPages;
|
||||
const isHidden = progressBarType === ProgressBarType.HIDDEN;
|
||||
const isMinimized = !isMaximized && !isDragging;
|
||||
|
||||
// the progress bar uses the reading direction to set the themes direction, thus, stuff has to be adjusted to still be correctly positioned
|
||||
// depending on which combination of theme direction and reading direction is currently active
|
||||
return (
|
||||
<ReaderProgressBarDirectionWrapper>
|
||||
<ReaderProgressBar
|
||||
direction={isHorizontal ? readerDirection : 'ltr'}
|
||||
progressBarPosition={finalProgressBarPosition}
|
||||
fullSegmentClicks
|
||||
createProgressBarSlot={useCallback(
|
||||
(
|
||||
page,
|
||||
_pagesIndex,
|
||||
primaryPageLoadState,
|
||||
secondaryPageLoadState,
|
||||
isCurrentPage,
|
||||
isLeadingPage,
|
||||
_isTrailingPage,
|
||||
_totalPages,
|
||||
showDraggingStyle,
|
||||
) => (
|
||||
<ReaderProgressBarSlotDesktop
|
||||
pageName={page.name}
|
||||
primaryPageLoadState={primaryPageLoadState}
|
||||
secondaryPageLoadState={secondaryPageLoadState}
|
||||
progressBarPosition={finalProgressBarPosition}
|
||||
isCurrentPage={isCurrentPage}
|
||||
isLeadingPage={isLeadingPage}
|
||||
showDraggingStyle={showDraggingStyle}
|
||||
/>
|
||||
),
|
||||
[finalProgressBarPosition],
|
||||
)}
|
||||
slotProps={{
|
||||
container: {
|
||||
sx: {
|
||||
overflow: 'hidden',
|
||||
transition: `all 0.${theme.transitions.duration.shortest}s`,
|
||||
...applyStyles(isHorizontal, {
|
||||
minHeight: '100px',
|
||||
...applyStyles(theme.direction === 'ltr', {
|
||||
left: readerDirection === 'ltr' ? readerNavBarWidth : scrollbarYSize,
|
||||
right: readerDirection === 'rtl' ? readerNavBarWidth : scrollbarYSize,
|
||||
}),
|
||||
...applyStyles(theme.direction === 'rtl', {
|
||||
left: readerDirection === 'rtl' ? readerNavBarWidth : scrollbarYSize,
|
||||
right: readerDirection === 'ltr' ? readerNavBarWidth : scrollbarYSize,
|
||||
}),
|
||||
}),
|
||||
...applyStyles(isVertical, {
|
||||
minWidth: '100px',
|
||||
bottom: `${scrollbarXSize}px`,
|
||||
}),
|
||||
...applyStyles(isBottom, {
|
||||
bottom: `${scrollbarXSize}px`,
|
||||
}),
|
||||
...applyStyles(isLeft, {
|
||||
...applyStyles(theme.direction === 'ltr', {
|
||||
left: readerDirection === 'ltr' ? readerNavBarWidth : 'unset',
|
||||
right: readerDirection === 'rtl' ? readerNavBarWidth : 'unset',
|
||||
}),
|
||||
...applyStyles(theme.direction === 'rtl', {
|
||||
right: readerDirection === 'rtl' ? scrollbarYSize : 'unset',
|
||||
left: readerDirection === 'ltr' ? scrollbarYSize : 'unset',
|
||||
}),
|
||||
}),
|
||||
...applyStyles(isRight, {
|
||||
...applyStyles(theme.direction === 'ltr', {
|
||||
right: readerDirection === 'ltr' ? scrollbarYSize : 'unset',
|
||||
left: readerDirection === 'rtl' ? scrollbarYSize : 'unset',
|
||||
}),
|
||||
...applyStyles(theme.direction === 'rtl', {
|
||||
left: readerDirection === 'rtl' ? readerNavBarWidth : 'unset',
|
||||
right: readerDirection === 'ltr' ? readerNavBarWidth : 'unset',
|
||||
}),
|
||||
}),
|
||||
...applyStyles(isMinimized, {
|
||||
opacity: !isHidden ? 0.85 : 0,
|
||||
}),
|
||||
...applyStyles(!arePagesLoaded, {
|
||||
pointerEvents: 'none',
|
||||
}),
|
||||
},
|
||||
onMouseEnter: () => setIsMaximized(true),
|
||||
onMouseLeave: () => setIsMaximized(false),
|
||||
},
|
||||
progressBarRoot: {
|
||||
sx: {
|
||||
gap: 0,
|
||||
transition: 'all 0.1s ease-in-out',
|
||||
backgroundColor: 'background.paper',
|
||||
...applyStyles(isVertical, {
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}),
|
||||
...applyStyles(isMinimized, {
|
||||
backgroundColor: 'unset',
|
||||
pointerEvents: 'none',
|
||||
...applyStyles(isHorizontal, {
|
||||
px: 2,
|
||||
}),
|
||||
...applyStyles(isVertical, {
|
||||
py: 2,
|
||||
}),
|
||||
...applyStyles(isBottom, {
|
||||
pb: 0.25,
|
||||
}),
|
||||
...applyStyles(isLeft, {
|
||||
...applyStyles(theme.direction === 'ltr', {
|
||||
pl: readerDirection === 'ltr' ? 0.25 : 0,
|
||||
pr: readerDirection === 'rtl' ? 0.25 : 0,
|
||||
}),
|
||||
...applyStyles(theme.direction === 'rtl', {
|
||||
pr: readerDirection === 'rtl' ? 0.25 : 0,
|
||||
pl: readerDirection === 'ltr' ? 0.25 : 0,
|
||||
}),
|
||||
}),
|
||||
...applyStyles(isRight, {
|
||||
...applyStyles(theme.direction === 'ltr', {
|
||||
pr: readerDirection === 'ltr' ? 0.25 : 0,
|
||||
pl: readerDirection === 'rtl' ? 0.25 : 0,
|
||||
}),
|
||||
...applyStyles(theme.direction === 'rtl', {
|
||||
pl: readerDirection === 'rtl' ? 0.25 : 0,
|
||||
pr: readerDirection === 'ltr' ? 0.25 : 0,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
progressBarSlotsContainer: {
|
||||
sx: {
|
||||
gap: 0.25,
|
||||
borderRadius: 2,
|
||||
transition: 'height 0.1s ease-in-out',
|
||||
...applyStyles(isHorizontal, {
|
||||
height: '20px',
|
||||
}),
|
||||
...applyStyles(isVertical, {
|
||||
flexDirection: 'column',
|
||||
width: '20px',
|
||||
}),
|
||||
...applyStyles(isMinimized, {
|
||||
...applyStyles(isHorizontal, {
|
||||
height: `${progressBarSize}px`,
|
||||
}),
|
||||
...applyStyles(isVertical, {
|
||||
width: `${progressBarSize}px`,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
progressBarReadPages: { sx: { display: 'none' } },
|
||||
progressBarCurrentPageSlot: { sx: { display: 'none' } },
|
||||
progressBarPageTexts: {
|
||||
base: {
|
||||
sx: {
|
||||
px: 1,
|
||||
py: 1.5,
|
||||
transition: 'all 0.1s ease-in-out',
|
||||
...applyStyles(isMinimized, {
|
||||
transform: 'scale(0)',
|
||||
p: 0,
|
||||
minWidth: 0,
|
||||
maxWidth: 0,
|
||||
minHeight: 0,
|
||||
maxHeight: 0,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ReaderProgressBarDirectionWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const StandardReaderProgressBar = withPropsFrom(
|
||||
memo(BaseStandardReaderProgressBar),
|
||||
[
|
||||
useNavBarContext,
|
||||
useReaderProgressBarContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
() => ({ readerDirection: ReaderService.useGetThemeDirection() }),
|
||||
useReaderScrollbarContext,
|
||||
userReaderStatePagesContext,
|
||||
],
|
||||
[
|
||||
'readerNavBarWidth',
|
||||
'isMaximized',
|
||||
'setIsMaximized',
|
||||
'isDragging',
|
||||
'progressBarType',
|
||||
'progressBarSize',
|
||||
'progressBarPosition',
|
||||
'progressBarPositionAutoVertical',
|
||||
'readerDirection',
|
||||
'scrollbarXSize',
|
||||
'scrollbarYSize',
|
||||
'totalPages',
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MediaQuery } from '@/features/core/utils/MediaQuery.tsx';
|
||||
|
||||
export const DefaultSettingFootnote = ({ areDefaultSettings }: { areDefaultSettings?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
|
||||
if (!isTouchDevice || areDefaultSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack sx={{ alignItems: 'end' }}>
|
||||
<Typography variant="caption">{t('reader.settings.default_setting_footnote')}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
62
src/features/reader/components/settings/ReaderSettings.tsx
Normal file
62
src/features/reader/components/settings/ReaderSettings.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderSettingsTabs } from '@/features/reader/components/settings/ReaderSettingsTabs.tsx';
|
||||
import { ReaderSettingTab } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { useDisableAllHotkeysWhileMounted } from '@/features/hotkeys/Hotkeys.utils.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
|
||||
export const ReaderSettings = ({ isOpen, close }: { isOpen: boolean; close: () => void }) => {
|
||||
const { manga } = useReaderStateMangaContext();
|
||||
const settings = ReaderService.useSettings();
|
||||
|
||||
useDisableAllHotkeysWhileMounted(isOpen);
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [isTransparent, setIsTransparent] = useState(false);
|
||||
|
||||
if (!manga) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
onClose={close}
|
||||
hideBackdrop={activeTab === ReaderSettingTab.FILTER}
|
||||
sx={{
|
||||
...applyStyles(isTransparent, {
|
||||
opacity: 0.75,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<ReaderSettingsTabs
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
settings={settings}
|
||||
updateSetting={(...args) => ReaderService.updateSetting(manga, ...args)}
|
||||
deleteSetting={(...args) => ReaderService.deleteSetting(manga, ...args)}
|
||||
setTransparent={setIsTransparent}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
193
src/features/reader/components/settings/ReaderSettingsTabs.tsx
Normal file
193
src/features/reader/components/settings/ReaderSettingsTabs.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TabsMenu } from '@/features/core/components/tabs/TabsMenu.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { MediaQuery } from '@/features/core/utils/MediaQuery.tsx';
|
||||
import { IReaderSettings, IReaderSettingsWithDefaultFlag } from '@/features/reader/types/Reader.types.ts';
|
||||
import { useReaderTapZoneContext } from '@/features/reader/contexts/ReaderTapZoneContext.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { READER_SETTING_TABS, ReaderSettingTab } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { TabPanel } from '@/features/core/components/tabs/TabPanel.tsx';
|
||||
import { ReaderLayoutSettings } from '@/features/reader/components/settings/layout/ReaderLayoutSettings.tsx';
|
||||
import { ReaderGeneralSettings } from '@/features/reader/components/settings/general/ReaderGeneralSettings.tsx';
|
||||
import { ReaderFilterSettings } from '@/features/reader/components/settings/filters/ReaderFilterSettings.tsx';
|
||||
import { ReaderBehaviourSettings } from '@/features/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx';
|
||||
import { ReaderDefaultLayoutSettings } from '@/features/reader/components/settings/layout/ReaderDefaultLayoutSettings.tsx';
|
||||
import { ReaderHotkeysSettings } from '@/features/reader/components/settings/hotkeys/ReaderHotkeysSettings.tsx';
|
||||
import { TReaderTapZoneContext } from '@/features/reader/types/TapZoneLayout.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
|
||||
const BaseReaderSettingsTabs = ({
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
areDefaultSettings,
|
||||
settings,
|
||||
updateSetting,
|
||||
deleteSetting,
|
||||
setShowPreview,
|
||||
mode: overlayMode,
|
||||
setTransparent,
|
||||
}: Pick<TReaderTapZoneContext, 'setShowPreview'> &
|
||||
Pick<ReturnType<typeof ReaderService.useOverlayMode>, 'mode'> & {
|
||||
activeTab: number;
|
||||
setActiveTab: (tab: number) => void;
|
||||
settings: IReaderSettingsWithDefaultFlag;
|
||||
updateSetting: (...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>) => void;
|
||||
areDefaultSettings?: boolean;
|
||||
deleteSetting: (setting: keyof IReaderSettings) => void;
|
||||
setTransparent?: (transparent: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isTouchDevice = MediaQuery.useIsTouchDevice();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabsMenu
|
||||
value={activeTab}
|
||||
onChange={(_, newTab) => setActiveTab(newTab)}
|
||||
sx={{
|
||||
...applyStyles(!!areDefaultSettings, { zIndex: 2 }),
|
||||
...applyStyles(!areDefaultSettings, {
|
||||
backgroundColor: 'background.paper',
|
||||
backgroundImage: 'var(--Paper-overlay)',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{Object.values(READER_SETTING_TABS).map(({ id, label, supportsTouchDevices }) => {
|
||||
if (!supportsTouchDevices && isTouchDevice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={id}
|
||||
value={id}
|
||||
label={t(label)}
|
||||
sx={{ flexGrow: 1, maxWidth: 'unset', textTransform: 'none' }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabsMenu>
|
||||
<Box sx={{ p: areDefaultSettings ? undefined : 2, overflowX: 'hidden' }}>
|
||||
{Object.values(READER_SETTING_TABS).map(({ id, supportsTouchDevices }) => {
|
||||
if (!supportsTouchDevices && isTouchDevice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (id as ReaderSettingTab) {
|
||||
case ReaderSettingTab.LAYOUT:
|
||||
if (areDefaultSettings) {
|
||||
return (
|
||||
<TabPanel key={id} index={id} currentIndex={activeTab}>
|
||||
<ReaderDefaultLayoutSettings
|
||||
readingMode={settings.readingMode}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TabPanel key={id} index={id} currentIndex={activeTab}>
|
||||
<ReaderLayoutSettings
|
||||
settings={settings}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
setShowPreview={setShowPreview!}
|
||||
isDefaultable={!areDefaultSettings}
|
||||
onDefault={(...args) => deleteSetting?.(...args)}
|
||||
isSeriesMode
|
||||
setTransparent={setTransparent}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
case ReaderSettingTab.GENERAL:
|
||||
return (
|
||||
<TabPanel
|
||||
key={id}
|
||||
index={id}
|
||||
currentIndex={activeTab}
|
||||
sx={{ p: areDefaultSettings ? 2 : undefined }}
|
||||
>
|
||||
<ReaderGeneralSettings
|
||||
overlayMode={overlayMode}
|
||||
settings={settings}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
// @ts-expect-error - TS2322: Type boolean is not assignable to type true
|
||||
isDefaultable={!areDefaultSettings}
|
||||
onDefault={(...args) => deleteSetting?.(...args)}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
case ReaderSettingTab.FILTER:
|
||||
return (
|
||||
<TabPanel
|
||||
key={id}
|
||||
index={id}
|
||||
currentIndex={activeTab}
|
||||
sx={{ p: areDefaultSettings ? 2 : undefined }}
|
||||
>
|
||||
<ReaderFilterSettings
|
||||
settings={settings}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
isDefaultable
|
||||
onDefault={(...args) => deleteSetting?.(...args)}
|
||||
setTransparent={setTransparent}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
case ReaderSettingTab.BEHAVIOUR:
|
||||
return (
|
||||
<TabPanel
|
||||
key={id}
|
||||
index={id}
|
||||
currentIndex={activeTab}
|
||||
sx={{ p: areDefaultSettings ? 2 : undefined }}
|
||||
>
|
||||
<ReaderBehaviourSettings
|
||||
settings={settings}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
// @ts-expect-error - TS2322: Type boolean is not assignable to type true
|
||||
isDefaultable={!areDefaultSettings}
|
||||
onDefault={(...args) => deleteSetting?.(...args)}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
case ReaderSettingTab.HOTKEYS:
|
||||
return (
|
||||
<TabPanel
|
||||
key={id}
|
||||
index={id}
|
||||
currentIndex={activeTab}
|
||||
sx={{ p: areDefaultSettings ? 2 : undefined }}
|
||||
>
|
||||
<ReaderHotkeysSettings
|
||||
settings={settings}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
isDefaultable
|
||||
onDefault={(...args) => deleteSetting?.(...args)}
|
||||
/>
|
||||
</TabPanel>
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unexpected "ReaderSettingTab" (${id})`);
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderSettingsTabs = withPropsFrom(
|
||||
BaseReaderSettingsTabs,
|
||||
[useReaderTapZoneContext, ReaderService.useOverlayMode],
|
||||
['setShowPreview', 'mode'],
|
||||
);
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { IReaderSettingsWithDefaultFlag, ReaderSettingsTypeProps } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { ReaderSettingExitMode } from '@/features/reader/components/settings/behaviour/ReaderSettingExitMode.tsx';
|
||||
import { isOffsetDoubleSpreadPagesEditable } from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import {
|
||||
DEFAULT_READER_SETTINGS,
|
||||
IMAGE_PRE_LOAD_AMOUNT,
|
||||
} from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { ReaderSettingAutoScroll } from '@/features/reader/components/settings/behaviour/ReaderSettingAutoScroll.tsx';
|
||||
import { ReaderSettingScrollAmount } from '@/features/reader/components/settings/behaviour/ReaderSettingScrollAmount.tsx';
|
||||
|
||||
export const ReaderBehaviourSettings = ({
|
||||
settings,
|
||||
updateSetting,
|
||||
onDefault,
|
||||
isDefaultable,
|
||||
}: {
|
||||
settings: IReaderSettingsWithDefaultFlag;
|
||||
updateSetting: (
|
||||
...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>
|
||||
) => ReturnType<typeof ReaderService.updateSetting>;
|
||||
} & ReaderSettingsTypeProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<ReaderSettingExitMode
|
||||
exitMode={settings.exitMode}
|
||||
setExitMode={(value) => updateSetting('exitMode', value)}
|
||||
/>
|
||||
<ReaderSettingScrollAmount
|
||||
scrollAmount={settings.scrollAmount}
|
||||
setScrollAmount={(value, commit) => updateSetting('scrollAmount', value, commit)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.label.skip_dup_chapters')}
|
||||
checked={settings.shouldSkipDupChapters}
|
||||
onChange={(_, checked) => updateSetting('shouldSkipDupChapters', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.label.skip_filtered_chapters')}</Typography>
|
||||
{isDefaultable && (
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.label.unchangeable_in_reader')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldSkipFilteredChapters}
|
||||
onChange={(_, checked) => updateSetting('shouldSkipFilteredChapters', checked)}
|
||||
disabled={isDefaultable}
|
||||
/>
|
||||
{isOffsetDoubleSpreadPagesEditable(settings.readingMode.value) && (
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.label.offset_double_spread')}
|
||||
checked={settings.shouldOffsetDoubleSpreads.value}
|
||||
onChange={(_, checked) => updateSetting('shouldOffsetDoubleSpreads', checked)}
|
||||
/>
|
||||
)}
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.infinite_scroll.title')}</Typography>
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.infinite_scroll.description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldUseInfiniteScroll}
|
||||
onChange={(_, checked) => updateSetting('shouldUseInfiniteScroll', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.preview.reading_mode.title')}</Typography>
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.preview.reading_mode.description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldShowReadingModePreview}
|
||||
onChange={(_, checked) => updateSetting('shouldShowReadingModePreview', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.preview.tap_zones.title')}</Typography>
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.preview.tap_zones.description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldShowTapZoneLayoutPreview}
|
||||
onChange={(_, checked) => updateSetting('shouldShowTapZoneLayoutPreview', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={
|
||||
<Box>
|
||||
<Typography>{t('reader.settings.auto_webtoon_mode.title')}</Typography>
|
||||
<Typography variant="body2" color="textDisabled">
|
||||
{t('reader.settings.auto_webtoon_mode.description')}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
checked={settings.shouldUseAutoWebtoonMode}
|
||||
onChange={(_, checked) => updateSetting('shouldUseAutoWebtoonMode', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.show_transition_page')}
|
||||
checked={settings.shouldShowTransitionPage}
|
||||
onChange={(_, checked) => updateSetting('shouldShowTransitionPage', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.chapter_transition.warning.missing_chapter')}
|
||||
checked={settings.shouldInformAboutMissingChapter}
|
||||
onChange={(_, checked) => updateSetting('shouldInformAboutMissingChapter', checked)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.chapter_transition.warning.scanlator_change')}
|
||||
checked={settings.shouldInformAboutScanlatorChange}
|
||||
onChange={(_, checked) => updateSetting('shouldInformAboutScanlatorChange', checked)}
|
||||
/>
|
||||
<ReaderSettingAutoScroll
|
||||
autoScroll={settings.autoScroll}
|
||||
setAutoScroll={(...args) => updateSetting('autoScroll', ...args)}
|
||||
/>
|
||||
<SliderInput
|
||||
label={t('reader.settings.image_preload_amount')}
|
||||
value={settings.imagePreLoadAmount}
|
||||
onDefault={() => onDefault?.('imagePreLoadAmount')}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.imagePreLoadAmount,
|
||||
value: settings.imagePreLoadAmount,
|
||||
step: IMAGE_PRE_LOAD_AMOUNT.step,
|
||||
min: IMAGE_PRE_LOAD_AMOUNT.min,
|
||||
max: IMAGE_PRE_LOAD_AMOUNT.max,
|
||||
onChange: (_, value) => {
|
||||
updateSetting('imagePreLoadAmount', value as number, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
updateSetting('imagePreLoadAmount', value as number, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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 '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { AUTO_SCROLL_SPEED, DEFAULT_READER_SETTINGS } from '@/features/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);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 { IReaderSettings, ReaderExitMode } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderExitMode> = {
|
||||
[ReaderExitMode.PREVIOUS]: {
|
||||
title: 'global.label.previous',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderExitMode.MANGA]: {
|
||||
title: 'manga.title_one',
|
||||
icon: null,
|
||||
},
|
||||
};
|
||||
|
||||
const READER_EXIT_MODE_VALUES = Object.values(ReaderExitMode).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderSettingExitMode = ({
|
||||
exitMode,
|
||||
setExitMode,
|
||||
}: Pick<IReaderSettings, 'exitMode'> & {
|
||||
setExitMode: (mode: ReaderExitMode) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.exit_mode')}
|
||||
value={exitMode}
|
||||
values={READER_EXIT_MODE_VALUES}
|
||||
setValue={setExitMode}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 { IReaderSettings, ReaderScrollAmount } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { DEFAULT_READER_SETTINGS, SCROLL_AMOUNT } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderScrollAmount> = {
|
||||
[ReaderScrollAmount.TINY]: {
|
||||
title: 'global.label.tiny',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderScrollAmount.SMALL]: {
|
||||
title: 'global.label.small',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderScrollAmount.MEDIUM]: {
|
||||
title: 'global.label.medium',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderScrollAmount.LARGE]: {
|
||||
title: 'global.label.large',
|
||||
icon: null,
|
||||
},
|
||||
};
|
||||
|
||||
const READER_SCROLL_AMOUNT_VALUES = Object.values(ReaderScrollAmount).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderSettingScrollAmount = ({
|
||||
scrollAmount,
|
||||
setScrollAmount,
|
||||
}: Pick<IReaderSettings, 'scrollAmount'> & {
|
||||
setScrollAmount: (amount: ReaderScrollAmount, commit: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.scroll_amount')}
|
||||
value={scrollAmount}
|
||||
values={READER_SCROLL_AMOUNT_VALUES}
|
||||
setValue={(value) => setScrollAmount(value as number, true)}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
<SliderInput
|
||||
label={t('reader.settings.label.custom_scroll_amount')}
|
||||
value={t('global.value', { value: scrollAmount, unit: '%' })}
|
||||
onDefault={() => setScrollAmount(DEFAULT_READER_SETTINGS.scrollAmount, true)}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.scrollAmount,
|
||||
value: scrollAmount,
|
||||
...SCROLL_AMOUNT,
|
||||
onChange: (_, value) => {
|
||||
setScrollAmount(value as number, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
setScrollAmount(value as number, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { ReaderSettingBrightness } from '@/features/reader/components/settings/filters/ReaderSettingBrightness.tsx';
|
||||
import { ReaderSettingContrast } from '@/features/reader/components/settings/filters/ReaderSettingContrast.tsx';
|
||||
import { ReaderSettingSaturate } from '@/features/reader/components/settings/filters/ReaderSettingSaturate.tsx';
|
||||
import { ReaderSettingHue } from '@/features/reader/components/settings/filters/ReaderSettingHue.tsx';
|
||||
import { ReaderSettingRGBA } from '@/features/reader/components/settings/filters/ReaderSettingRGBA.tsx';
|
||||
import { ReaderSettingSepia } from '@/features/reader/components/settings/filters/ReaderSettingSepia.tsx';
|
||||
import { ReaderSettingGrayscale } from '@/features/reader/components/settings/filters/ReaderSettingGrayscale.tsx';
|
||||
import { ReaderSettingInvert } from '@/features/reader/components/settings/filters/ReaderSettingInvert.tsx';
|
||||
import { ReaderSettingsTypeProps } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ResetButton } from '@/features/core/components/buttons/ResetButton.tsx';
|
||||
|
||||
export const ReaderFilterSettings = ({
|
||||
settings: { customFilter },
|
||||
updateSetting,
|
||||
onDefault,
|
||||
setTransparent,
|
||||
}: ReaderSettingsTypeProps) => {
|
||||
const update = (value: any, commit?: boolean) => {
|
||||
updateSetting('customFilter', value, commit);
|
||||
setTransparent?.(commit === undefined ? false : !commit);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<ReaderSettingBrightness
|
||||
brightness={customFilter.brightness}
|
||||
updateSetting={(key, value, commit) =>
|
||||
update(
|
||||
{
|
||||
...customFilter,
|
||||
[key]: value,
|
||||
},
|
||||
commit,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ReaderSettingContrast
|
||||
contrast={customFilter.contrast}
|
||||
updateSetting={(key, value, commit) =>
|
||||
update(
|
||||
{
|
||||
...customFilter,
|
||||
[key]: value,
|
||||
},
|
||||
commit,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ReaderSettingSaturate
|
||||
saturate={customFilter.saturate}
|
||||
updateSetting={(key, value, commit) =>
|
||||
update(
|
||||
{
|
||||
...customFilter,
|
||||
[key]: value,
|
||||
},
|
||||
commit,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ReaderSettingHue
|
||||
hue={customFilter.hue}
|
||||
updateSetting={(key, value, commit) =>
|
||||
update(
|
||||
{
|
||||
...customFilter,
|
||||
[key]: value,
|
||||
},
|
||||
commit,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ReaderSettingRGBA
|
||||
rgba={customFilter.rgba}
|
||||
updateSetting={(key, value, commit) =>
|
||||
update(
|
||||
{
|
||||
...customFilter,
|
||||
[key]: value,
|
||||
},
|
||||
commit,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ReaderSettingSepia
|
||||
sepia={customFilter.sepia}
|
||||
updateSetting={(value) =>
|
||||
update({
|
||||
...customFilter,
|
||||
sepia: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ReaderSettingGrayscale
|
||||
grayscale={customFilter.grayscale}
|
||||
updateSetting={(value) =>
|
||||
update({
|
||||
...customFilter,
|
||||
grayscale: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ReaderSettingInvert
|
||||
invert={customFilter.invert}
|
||||
updateSetting={(value) =>
|
||||
update({
|
||||
...customFilter,
|
||||
invert: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Stack sx={{ alignItems: 'end' }}>
|
||||
<ResetButton onClick={() => onDefault?.('customFilter')} variant="outlined" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
|
||||
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderSettingBrightness = ({
|
||||
brightness,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings['customFilter'], 'brightness'> & {
|
||||
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
|
||||
filter: Filter,
|
||||
value: IReaderSettings['customFilter'][Filter],
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.brightness')}
|
||||
checked={brightness.enabled}
|
||||
onChange={(_, checked) => updateSetting('brightness', { ...brightness, enabled: checked }, true)}
|
||||
/>
|
||||
{brightness.enabled && (
|
||||
<SliderInput
|
||||
label={t('reader.settings.custom_filter.brightness')}
|
||||
value={brightness.value}
|
||||
onDefault={() =>
|
||||
updateSetting(
|
||||
'brightness',
|
||||
{ ...brightness, value: DEFAULT_READER_SETTINGS.customFilter.brightness.value },
|
||||
true,
|
||||
)
|
||||
}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.customFilter.brightness.value,
|
||||
value: brightness.value,
|
||||
step: CUSTOM_FILTER.brightness.step,
|
||||
min: CUSTOM_FILTER.brightness.min,
|
||||
max: CUSTOM_FILTER.brightness.max,
|
||||
onChange: (_, value) => {
|
||||
updateSetting('brightness', { ...brightness, value: value as number }, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
updateSetting('brightness', { ...brightness, value: value as number }, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderSettingContrast = ({
|
||||
contrast,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings['customFilter'], 'contrast'> & {
|
||||
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
|
||||
filter: Filter,
|
||||
value: IReaderSettings['customFilter'][Filter],
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.contrast')}
|
||||
checked={contrast.enabled}
|
||||
onChange={(_, checked) => updateSetting('contrast', { ...contrast, enabled: checked }, true)}
|
||||
/>
|
||||
{contrast.enabled && (
|
||||
<SliderInput
|
||||
label={t('reader.settings.custom_filter.contrast')}
|
||||
value={contrast.value}
|
||||
onDefault={() =>
|
||||
updateSetting(
|
||||
'contrast',
|
||||
{ ...contrast, value: DEFAULT_READER_SETTINGS.customFilter.contrast.value },
|
||||
true,
|
||||
)
|
||||
}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.customFilter.contrast.value,
|
||||
value: contrast.value,
|
||||
step: CUSTOM_FILTER.contrast.step,
|
||||
min: CUSTOM_FILTER.contrast.min,
|
||||
max: CUSTOM_FILTER.contrast.max,
|
||||
onChange: (_, newValue) => {
|
||||
updateSetting('contrast', { ...contrast, value: newValue as number }, false);
|
||||
},
|
||||
onChangeCommitted: (_, newValue) => {
|
||||
updateSetting('contrast', { ...contrast, value: newValue as number }, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
|
||||
export const ReaderSettingGrayscale = ({
|
||||
grayscale,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings['customFilter'], 'grayscale'> & {
|
||||
updateSetting: (grayscale: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.grayscale')}
|
||||
checked={grayscale}
|
||||
onChange={(_, checked) => updateSetting(checked)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderSettingHue = ({
|
||||
hue,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings['customFilter'], 'hue'> & {
|
||||
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
|
||||
filter: Filter,
|
||||
value: IReaderSettings['customFilter'][Filter],
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.hue')}
|
||||
checked={hue.enabled}
|
||||
onChange={(_, checked) => updateSetting('hue', { ...hue, enabled: checked }, true)}
|
||||
/>
|
||||
{hue.enabled && (
|
||||
<SliderInput
|
||||
label={t('reader.settings.custom_filter.hue')}
|
||||
value={hue.value}
|
||||
onDefault={() =>
|
||||
updateSetting('hue', { ...hue, value: DEFAULT_READER_SETTINGS.customFilter.hue.value }, true)
|
||||
}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.customFilter.hue.value,
|
||||
value: hue.value,
|
||||
step: CUSTOM_FILTER.hue.step,
|
||||
min: CUSTOM_FILTER.hue.min,
|
||||
max: CUSTOM_FILTER.hue.max,
|
||||
onChange: (_, newValue) => {
|
||||
updateSetting('hue', { ...hue, value: newValue as number }, false);
|
||||
},
|
||||
onChangeCommitted: (_, newValue) => {
|
||||
updateSetting('hue', { ...hue, value: newValue as number }, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
|
||||
export const ReaderSettingInvert = ({
|
||||
invert,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings['customFilter'], 'invert'> & {
|
||||
updateSetting: (invert: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.invert')}
|
||||
checked={invert}
|
||||
onChange={(_, checked) => updateSetting(checked)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
|
||||
import {
|
||||
CUSTOM_FILTER,
|
||||
DEFAULT_READER_SETTINGS,
|
||||
READER_BLEND_MODE_VALUE_TO_DISPLAY_DATA,
|
||||
READER_BLEND_MODE_VALUES,
|
||||
} from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
type RGBAType = Exclude<keyof IReaderSettings['customFilter']['rgba']['value'], 'blendMode'>;
|
||||
|
||||
const RGBA_TYPE_TO_TRANSLATION_KEY: Record<RGBAType, TranslationKey> = {
|
||||
red: 'reader.settings.custom_filter.rgba.red',
|
||||
green: 'reader.settings.custom_filter.rgba.green',
|
||||
blue: 'reader.settings.custom_filter.rgba.blue',
|
||||
alpha: 'reader.settings.custom_filter.rgba.alpha',
|
||||
};
|
||||
|
||||
export const ReaderSettingRGBA = ({
|
||||
rgba,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings['customFilter'], 'rgba'> & {
|
||||
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
|
||||
filter: Filter,
|
||||
value: IReaderSettings['customFilter'][Filter],
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.rgba.title')}
|
||||
checked={rgba.enabled}
|
||||
onChange={(_, checked) => updateSetting('rgba', { ...rgba, enabled: checked }, true)}
|
||||
/>
|
||||
{rgba.enabled && (
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
{Object.entries(rgba.value).map(([key, value]) => {
|
||||
if (key === 'blendMode') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SliderInput
|
||||
key={key}
|
||||
label={t(RGBA_TYPE_TO_TRANSLATION_KEY[key as RGBAType])}
|
||||
value={value}
|
||||
onDefault={() =>
|
||||
updateSetting(
|
||||
'rgba',
|
||||
{
|
||||
...rgba,
|
||||
value: {
|
||||
...rgba.value,
|
||||
[key]: DEFAULT_READER_SETTINGS.customFilter.rgba.value[key as RGBAType],
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
slotProps={{
|
||||
slider: {
|
||||
value,
|
||||
defaultValue: DEFAULT_READER_SETTINGS.customFilter.rgba.value[key as RGBAType],
|
||||
step: CUSTOM_FILTER.rgba[key as RGBAType].step,
|
||||
min: CUSTOM_FILTER.rgba[key as RGBAType].min,
|
||||
max: CUSTOM_FILTER.rgba[key as RGBAType].max,
|
||||
onChange: (_, newValue) => {
|
||||
updateSetting(
|
||||
'rgba',
|
||||
{ ...rgba, value: { ...rgba.value, [key]: newValue } },
|
||||
false,
|
||||
);
|
||||
},
|
||||
onChangeCommitted: (_, newValue) => {
|
||||
updateSetting(
|
||||
'rgba',
|
||||
{ ...rgba, value: { ...rgba.value, [key]: newValue } },
|
||||
true,
|
||||
);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.custom_filter.rgba.blend_mode.title')}
|
||||
value={rgba.value.blendMode}
|
||||
values={READER_BLEND_MODE_VALUES}
|
||||
setValue={(value) =>
|
||||
updateSetting(
|
||||
'rgba',
|
||||
{
|
||||
...rgba,
|
||||
value: { ...rgba.value, blendMode: value },
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
valueToDisplayData={READER_BLEND_MODE_VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { CUSTOM_FILTER, DEFAULT_READER_SETTINGS } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderSettingSaturate = ({
|
||||
saturate,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings['customFilter'], 'saturate'> & {
|
||||
updateSetting: <Filter extends keyof IReaderSettings['customFilter']>(
|
||||
filter: Filter,
|
||||
value: IReaderSettings['customFilter'][Filter],
|
||||
commit: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.saturate')}
|
||||
checked={saturate.enabled}
|
||||
onChange={(_, checked) => updateSetting('saturate', { ...saturate, enabled: checked }, true)}
|
||||
/>
|
||||
{saturate.enabled && (
|
||||
<SliderInput
|
||||
label={t('reader.settings.custom_filter.saturate')}
|
||||
value={saturate.value}
|
||||
onDefault={() =>
|
||||
updateSetting(
|
||||
'saturate',
|
||||
{ ...saturate, value: DEFAULT_READER_SETTINGS.customFilter.saturate.value },
|
||||
true,
|
||||
)
|
||||
}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.customFilter.saturate.value,
|
||||
value: saturate.value,
|
||||
step: CUSTOM_FILTER.saturate.step,
|
||||
min: CUSTOM_FILTER.saturate.min,
|
||||
max: CUSTOM_FILTER.saturate.max,
|
||||
onChange: (_, value) => {
|
||||
updateSetting('saturate', { ...saturate, value: value as number }, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
updateSetting('saturate', { ...saturate, value: value as number }, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
|
||||
export const ReaderSettingSepia = ({
|
||||
sepia,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings['customFilter'], 'sepia'> & {
|
||||
updateSetting: (sepia: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.custom_filter.sepia')}
|
||||
checked={sepia}
|
||||
onChange={(_, checked) => updateSetting(checked)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReaderSettingProgressBarType } from '@/features/reader/components/settings/general/ReaderSettingProgressBarType.tsx';
|
||||
import { ReaderSettingProgressBarSize } from '@/features/reader/components/settings/general/ReaderSettingProgressBarSize.tsx';
|
||||
import { ReaderSettingProgressBarPosition } from '@/features/reader/components/settings/general/ReaderSettingProgressBarPosition.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ProgressBarType,
|
||||
ReaderOverlayMode,
|
||||
ReaderSettingsTypeProps,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { ReaderSettingOverlayMode } from '@/features/reader/components/settings/general/ReaderSettingOverlayMode.tsx';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { ReaderSettingBackgroundColor } from '@/features/reader/components/settings/general/ReaderSettingBackgroundColor.tsx';
|
||||
|
||||
export const ReaderGeneralSettings = ({
|
||||
overlayMode,
|
||||
settings,
|
||||
updateSetting,
|
||||
onDefault,
|
||||
}: Pick<IReaderSettings, 'overlayMode'> & ReaderSettingsTypeProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<ReaderSettingOverlayMode
|
||||
overlayMode={settings.overlayMode}
|
||||
setOverlayMode={(value) => updateSetting('overlayMode', value)}
|
||||
/>
|
||||
<ReaderSettingBackgroundColor
|
||||
backgroundColor={settings.backgroundColor}
|
||||
updateSetting={(value) => updateSetting('backgroundColor', value)}
|
||||
/>
|
||||
<ReaderSettingProgressBarType
|
||||
overlayMode={overlayMode}
|
||||
progressBarType={settings.progressBarType}
|
||||
setProgressBarType={(value) => updateSetting('progressBarType', value)}
|
||||
/>
|
||||
<ReaderSettingProgressBarSize
|
||||
overlayMode={overlayMode}
|
||||
progressBarType={settings.progressBarType}
|
||||
progressBarSize={settings.progressBarSize}
|
||||
setProgressBarSize={(...args) => updateSetting('progressBarSize', ...args)}
|
||||
onDefault={() => onDefault?.('progressBarSize')}
|
||||
/>
|
||||
<ReaderSettingProgressBarPosition
|
||||
progressBarPosition={settings.progressBarPosition}
|
||||
progressBarPositionAutoVertical={settings.progressBarPositionAutoVertical}
|
||||
updateSetting={updateSetting}
|
||||
/>
|
||||
{(settings.progressBarType === ProgressBarType.HIDDEN || overlayMode === ReaderOverlayMode.MOBILE) && (
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.label.show_page_number')}
|
||||
checked={settings.shouldShowPageNumber}
|
||||
onChange={(_, checked) => updateSetting('shouldShowPageNumber', checked)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 { ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, ReaderBackgroundColor } from '@/features/reader/types/Reader.types.ts';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderBackgroundColor> = {
|
||||
[ReaderBackgroundColor.THEME]: {
|
||||
title: 'settings.appearance.theme.title',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBackgroundColor.BLACK]: {
|
||||
title: 'global.colors.black',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBackgroundColor.GRAY]: {
|
||||
title: 'global.colors.gray',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBackgroundColor.WHITE]: {
|
||||
title: 'global.colors.white',
|
||||
icon: null,
|
||||
},
|
||||
};
|
||||
|
||||
const READER_BACKGROUND_COLOR_VALUES = Object.values(ReaderBackgroundColor).filter(
|
||||
(value) => typeof value === 'number',
|
||||
);
|
||||
|
||||
export const ReaderSettingBackgroundColor = ({
|
||||
backgroundColor,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'backgroundColor'> & {
|
||||
updateSetting: (color: ReaderBackgroundColor) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.background_color')}
|
||||
value={backgroundColor}
|
||||
values={READER_BACKGROUND_COLOR_VALUES}
|
||||
setValue={updateSetting}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
|
||||
import ComputerIcon from '@mui/icons-material/Computer';
|
||||
import AutoModeIcon from '@mui/icons-material/AutoMode';
|
||||
import { IReaderSettings, ReaderOverlayMode } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderOverlayMode> = {
|
||||
[ReaderOverlayMode.AUTO]: {
|
||||
title: 'global.label.auto',
|
||||
icon: <AutoModeIcon />,
|
||||
},
|
||||
[ReaderOverlayMode.DESKTOP]: {
|
||||
title: 'global.label.desktop',
|
||||
icon: <ComputerIcon />,
|
||||
},
|
||||
[ReaderOverlayMode.MOBILE]: {
|
||||
title: 'global.label.mobile',
|
||||
icon: <PhoneIphoneIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const READING_MODE_VALUES = Object.values(ReaderOverlayMode).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderSettingOverlayMode = ({
|
||||
overlayMode,
|
||||
setOverlayMode,
|
||||
}: Pick<IReaderSettings, 'overlayMode'> & {
|
||||
setOverlayMode: (mode: ReaderOverlayMode) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.overlay_mode')}
|
||||
value={overlayMode}
|
||||
values={READING_MODE_VALUES}
|
||||
setValue={setOverlayMode}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||
import AutoModeIcon from '@mui/icons-material/AutoMode';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ProgressBarPosition,
|
||||
ProgressBarPositionAutoVertical,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ProgressBarPosition> = {
|
||||
[ProgressBarPosition.AUTO]: {
|
||||
title: 'global.label.auto',
|
||||
icon: <AutoModeIcon />,
|
||||
},
|
||||
[ProgressBarPosition.BOTTOM]: {
|
||||
title: 'global.label.bottom',
|
||||
icon: <ArrowBackIosNewIcon sx={{ transform: 'rotate(90deg)' }} />,
|
||||
},
|
||||
[ProgressBarPosition.LEFT]: {
|
||||
title: 'global.label.left',
|
||||
icon: <ArrowForwardIosIcon />,
|
||||
},
|
||||
[ProgressBarPosition.RIGHT]: {
|
||||
title: 'global.label.right',
|
||||
icon: <ArrowBackIosNewIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const PROGRESS_BAR_POSITION_VALUES = Object.values(ProgressBarPosition).filter((value) => typeof value === 'number');
|
||||
|
||||
const PROGRESS_BAR_AUTO_VERTICAL_POSITION_VALUES = Object.values(
|
||||
ProgressBarPositionAutoVertical,
|
||||
) as unknown as TupleUnion<keyof typeof ProgressBarPositionAutoVertical>;
|
||||
|
||||
export const ReaderSettingProgressBarPosition = ({
|
||||
progressBarPosition,
|
||||
progressBarPositionAutoVertical,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettings, 'progressBarPosition' | 'progressBarPositionAutoVertical'> & {
|
||||
updateSetting: <
|
||||
Position extends keyof Pick<IReaderSettings, 'progressBarPosition' | 'progressBarPositionAutoVertical'>,
|
||||
>(
|
||||
filter: Position,
|
||||
value: IReaderSettings[Position],
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isAutoPosition = progressBarPosition === ProgressBarPosition.AUTO;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.progress_bar.position')}
|
||||
value={progressBarPosition}
|
||||
values={PROGRESS_BAR_POSITION_VALUES}
|
||||
setValue={(position) => updateSetting('progressBarPosition', position)}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
{isAutoPosition && (
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.progress_bar.auto_vertical_position.title')}
|
||||
value={progressBarPositionAutoVertical}
|
||||
values={PROGRESS_BAR_AUTO_VERTICAL_POSITION_VALUES}
|
||||
setValue={(position) => updateSetting('progressBarPositionAutoVertical', position)}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 { IReaderSettings, ProgressBarType, ReaderOverlayMode } from '@/features/reader/types/Reader.types.ts';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { AUTO_SCROLL_SPEED, DEFAULT_READER_SETTINGS } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderSettingProgressBarSize = ({
|
||||
overlayMode,
|
||||
progressBarType,
|
||||
progressBarSize,
|
||||
setProgressBarSize,
|
||||
onDefault,
|
||||
}: Pick<IReaderSettings, 'progressBarType' | 'progressBarSize' | 'overlayMode'> & {
|
||||
setProgressBarSize: (size: number, commit: boolean) => void;
|
||||
onDefault: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isChangeable = overlayMode === ReaderOverlayMode.DESKTOP && progressBarType === ProgressBarType.STANDARD;
|
||||
|
||||
if (!isChangeable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SliderInput
|
||||
label={t('reader.settings.progress_bar.size')}
|
||||
value={t('global.value', { value: progressBarSize, unit: t('global.unit.px') })}
|
||||
onDefault={onDefault}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.progressBarSize,
|
||||
value: progressBarSize,
|
||||
step: AUTO_SCROLL_SPEED.step,
|
||||
min: AUTO_SCROLL_SPEED.min,
|
||||
max: AUTO_SCROLL_SPEED.max,
|
||||
onChange: (_, value) => {
|
||||
setProgressBarSize(value as number, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
setProgressBarSize(value as number, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { IReaderSettings, ProgressBarType, ReaderOverlayMode } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { HiddenProgressBarIcon } from '@/assets/icons/svg/HiddenProgressBarIcon.tsx';
|
||||
import { StandardProgressBarIcon } from '@/assets/icons/svg/StandardProgressBarIcon.tsx';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ProgressBarType> = {
|
||||
[ProgressBarType.HIDDEN]: {
|
||||
title: 'global.label.hidden',
|
||||
icon: <HiddenProgressBarIcon />,
|
||||
},
|
||||
[ProgressBarType.STANDARD]: {
|
||||
title: 'global.label.standard',
|
||||
icon: <StandardProgressBarIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const PROGRESS_BAR_TYPE_VALUES = Object.values(ProgressBarType).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderSettingProgressBarType = ({
|
||||
overlayMode,
|
||||
progressBarType,
|
||||
setProgressBarType,
|
||||
}: Pick<IReaderSettings, 'progressBarType' | 'overlayMode'> & {
|
||||
setProgressBarType: (progressBarType: ProgressBarType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (overlayMode !== ReaderOverlayMode.DESKTOP) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.progress_bar.style')}
|
||||
value={progressBarType}
|
||||
values={PROGRESS_BAR_TYPE_VALUES}
|
||||
setValue={setProgressBarType}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
45
src/features/reader/components/settings/hotkeys/Hotkey.tsx
Normal file
45
src/features/reader/components/settings/hotkeys/Hotkey.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 { Fragment } from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { Kbd } from '@/features/core/components/texts/Kbd.tsx';
|
||||
|
||||
export const Hotkey = ({ keys, removeKey }: { keys: string[]; removeKey?: (key: string) => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
|
||||
{keys.map((key, index) => (
|
||||
<Fragment key={key}>
|
||||
<CustomTooltip title={t('global.button.delete')} hidden={!removeKey}>
|
||||
<Stack
|
||||
sx={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 0.5,
|
||||
cursor: removeKey ? 'pointer' : undefined,
|
||||
}}
|
||||
onClick={() => removeKey?.(key)}
|
||||
>
|
||||
{key.split('+').map((splitKey, splitIndex, splitKeys) => (
|
||||
<Fragment key={splitKey}>
|
||||
<Kbd>{splitKey}</Kbd>
|
||||
{splitIndex === splitKeys.length - 1 ? '' : '+'}
|
||||
</Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
</CustomTooltip>
|
||||
{index === keys.length - 1 ? '' : ','}
|
||||
</Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ReaderSettingsTypeProps } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ReaderSettingHotkey } from '@/features/reader/components/settings/hotkeys/ReaderSettingHotkey.tsx';
|
||||
import { READER_HOTKEYS } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { ResetButton } from '@/features/core/components/buttons/ResetButton.tsx';
|
||||
|
||||
export const ReaderHotkeysSettings = ({ settings, updateSetting, onDefault }: ReaderSettingsTypeProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<Stack sx={{ alignItems: 'end' }}>
|
||||
<Typography variant="caption">{t('hotkeys.info.delete')}</Typography>
|
||||
</Stack>
|
||||
{READER_HOTKEYS.map((hotkey) => (
|
||||
<ReaderSettingHotkey
|
||||
key={hotkey}
|
||||
hotkey={hotkey}
|
||||
keys={settings.hotkeys[hotkey]}
|
||||
existingKeys={Object.values(settings.hotkeys).flat()}
|
||||
updateSetting={(keys) => updateSetting('hotkeys', { ...settings.hotkeys, [hotkey]: keys })}
|
||||
/>
|
||||
))}
|
||||
<Stack sx={{ alignItems: 'end' }}>
|
||||
<ResetButton onClick={() => onDefault?.('hotkeys')} variant="outlined" />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { ReaderHotkey } from '@/features/reader/types/Reader.types.ts';
|
||||
import { DEFAULT_READER_SETTINGS } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
import { RecordHotkey } from '@/features/reader/components/settings/hotkeys/RecordHotkey.tsx';
|
||||
import { Hotkey } from '@/features/reader/components/settings/hotkeys/Hotkey.tsx';
|
||||
import { ResetButton } from '@/features/core/components/buttons/ResetButton.tsx';
|
||||
|
||||
const READER_HOTKEY_TO_TITLE: Record<ReaderHotkey, TranslationKey> = {
|
||||
[ReaderHotkey.PREVIOUS_PAGE]: 'reader.settings.hotkey.previous_page',
|
||||
[ReaderHotkey.NEXT_PAGE]: 'reader.settings.hotkey.next_page',
|
||||
[ReaderHotkey.SCROLL_BACKWARD]: 'reader.settings.hotkey.scroll_backward',
|
||||
[ReaderHotkey.SCROLL_FORWARD]: 'reader.settings.hotkey.scroll_forward',
|
||||
[ReaderHotkey.PREVIOUS_CHAPTER]: 'reader.settings.hotkey.previous_chapter',
|
||||
[ReaderHotkey.NEXT_CHAPTER]: 'reader.settings.hotkey.next_chapter',
|
||||
[ReaderHotkey.TOGGLE_MENU]: 'reader.settings.hotkey.menu',
|
||||
[ReaderHotkey.CYCLE_SCALE_TYPE]: 'reader.settings.hotkey.scale_type',
|
||||
[ReaderHotkey.STRETCH_IMAGE]: 'reader.settings.hotkey.stretch_image',
|
||||
[ReaderHotkey.OFFSET_SPREAD_PAGES]: 'reader.settings.hotkey.offset_spread_pages',
|
||||
[ReaderHotkey.CYCLE_READING_MODE]: 'reader.settings.hotkey.reading_mode',
|
||||
[ReaderHotkey.CYCLE_READING_DIRECTION]: 'reader.settings.hotkey.reading_direction',
|
||||
[ReaderHotkey.TOGGLE_AUTO_SCROLL]: 'reader.settings.hotkey.auto_scroll',
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE]: 'reader.settings.hotkey.auto_scroll_speed_increase',
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE]: 'reader.settings.hotkey.auto_scroll_speed_decrease',
|
||||
[ReaderHotkey.EXIT_READER]: 'reader.button.exit',
|
||||
};
|
||||
|
||||
export const ReaderSettingHotkey = ({
|
||||
hotkey,
|
||||
keys,
|
||||
existingKeys,
|
||||
updateSetting,
|
||||
}: {
|
||||
hotkey: ReaderHotkey;
|
||||
keys: string[];
|
||||
existingKeys: string[];
|
||||
updateSetting: (keys: string[]) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const popupState = usePopupState({ popupId: 'reader-setting-record-hotkey', variant: 'dialog' });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack sx={{ flexDirection: 'row', alignItems: 'center', gap: 1 }}>
|
||||
<Typography sx={{ flexGrow: 1 }}>{t(READER_HOTKEY_TO_TITLE[hotkey])}</Typography>
|
||||
<Hotkey
|
||||
keys={keys}
|
||||
removeKey={(keyToRemove) => updateSetting(keys.filter((key) => key !== keyToRemove))}
|
||||
/>
|
||||
<CustomTooltip title={t('global.button.add')}>
|
||||
<IconButton {...bindTrigger(popupState)} color="inherit">
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<ResetButton asIconButton onClick={() => updateSetting(DEFAULT_READER_SETTINGS.hotkeys[hotkey])} />
|
||||
</Stack>
|
||||
{popupState.isOpen && (
|
||||
<RecordHotkey
|
||||
onClose={popupState.close}
|
||||
onCreate={(recordedKeys) => updateSetting([...keys, ...recordedKeys])}
|
||||
existingKeys={existingKeys}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 { useEffect } from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useRecordHotkeys } from 'react-hotkeys-hook';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Hotkey } from '@/features/reader/components/settings/hotkeys/Hotkey.tsx';
|
||||
|
||||
export const RecordHotkey = ({
|
||||
onClose,
|
||||
onCreate,
|
||||
existingKeys,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onCreate: (keys: string[]) => void;
|
||||
existingKeys: string[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [recordedKeys, { start, stop, resetKeys }] = useRecordHotkeys();
|
||||
const keys = [[...recordedKeys].join('+')];
|
||||
const isExistingKey = keys.some((key) =>
|
||||
existingKeys.map((existingKey) => existingKey.toLowerCase()).includes(key.toLowerCase()),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
start();
|
||||
|
||||
return () => {
|
||||
stop();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onClose} fullWidth>
|
||||
<DialogTitle>{t('hotkeys.create.dialog.title')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }}>
|
||||
<Trans
|
||||
i18nKey="hotkeys.create.dialog.label"
|
||||
components={{
|
||||
Keys: recordedKeys.size ? (
|
||||
<Hotkey keys={keys} />
|
||||
) : (
|
||||
<Typography>{t('hotkeys.create.dialog.placeholder')}</Typography>
|
||||
),
|
||||
}}
|
||||
>
|
||||
Recorded keys:
|
||||
</Trans>
|
||||
</Stack>
|
||||
{isExistingKey && <Typography color="error">{t('hotkeys.create.error.exists')}</Typography>}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Button onClick={resetKeys}>{t('global.button.reset')}</Button>
|
||||
<Stack direction="row">
|
||||
<Button onClick={onClose}>{t('global.button.cancel')}</Button>
|
||||
<Button
|
||||
disabled={isExistingKey}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onCreate(keys);
|
||||
}}
|
||||
>
|
||||
{t('global.button.create')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { ComponentProps } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReaderLayoutSettings } from '@/features/reader/components/settings/layout/ReaderLayoutSettings.tsx';
|
||||
import { ReaderSettingProfileSettings } from '@/features/reader/components/settings/layout/profiles/ReaderSettingProfileSettings.tsx';
|
||||
import {
|
||||
READING_MODE_VALUE_TO_DISPLAY_DATA,
|
||||
READING_MODE_VALUES,
|
||||
} from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { ReaderSettingReadingMode } from '@/features/reader/components/settings/layout/ReaderSettingReadingMode.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/types/Reader.types.ts';
|
||||
|
||||
export const ReaderDefaultLayoutSettings = ({
|
||||
profiles = READING_MODE_VALUES,
|
||||
readingMode,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof ReaderLayoutSettings>, 'setShowPreview' | 'settings'> & {
|
||||
profiles?: ReadingMode[];
|
||||
readingMode?: IReaderSettingsWithDefaultFlag['readingMode'];
|
||||
}) => {
|
||||
const { updateSetting, isSeriesMode } = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2, pb: Number(!isSeriesMode) * 2 }}>
|
||||
{readingMode !== undefined && (
|
||||
<Stack sx={{ pt: 2, px: 2 }}>
|
||||
<ReaderSettingReadingMode
|
||||
readingMode={readingMode}
|
||||
setReadingMode={(value) => updateSetting('readingMode', value)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{profiles.map((profile) => (
|
||||
<ReaderSettingProfileSettings
|
||||
key={profile}
|
||||
profile={profile}
|
||||
title={t(READING_MODE_VALUE_TO_DISPLAY_DATA[profile].title)}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReaderSettingReadingMode } from '@/features/reader/components/settings/layout/ReaderSettingReadingMode.tsx';
|
||||
import { ReaderSettingReadingDirection } from '@/features/reader/components/settings/layout/ReaderSettingReadingDirection.tsx';
|
||||
import { ReaderSettingTapZoneLayout } from '@/features/reader/components/settings/layout/ReaderSettingTapZoneLayout.tsx';
|
||||
import { ReaderSettingTapZoneInvertMode } from '@/features/reader/components/settings/layout/ReaderSettingTapZoneInvertMode.tsx';
|
||||
import { ReaderSettingPageScaleMode } from '@/features/reader/components/settings/layout/ReaderSettingPageScaleMode.tsx';
|
||||
import { ReaderSettingStretchPage } from '@/features/reader/components/settings/layout/ReaderSettingStretchPage.tsx';
|
||||
import { ReaderSettingsTypeProps } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ReaderSettingPageGap } from '@/features/reader/components/settings/layout/ReaderSettingPageGap.tsx';
|
||||
import { ReaderSettingWidth } from '@/features/reader/components/settings/layout/ReaderSettingWidth.tsx';
|
||||
import { DefaultSettingFootnote } from '@/features/reader/components/settings/DefaultSettingFootnote.tsx';
|
||||
import { TReaderTapZoneContext } from '@/features/reader/types/TapZoneLayout.types.ts';
|
||||
import { ReaderDefaultLayoutSettings } from '@/features/reader/components/settings/layout/ReaderDefaultLayoutSettings.tsx';
|
||||
|
||||
export const ReaderLayoutSettings = ({
|
||||
setShowPreview,
|
||||
settings,
|
||||
updateSetting,
|
||||
isDefaultable,
|
||||
onDefault,
|
||||
isSeriesMode,
|
||||
setTransparent,
|
||||
}: ReaderSettingsTypeProps & {
|
||||
setShowPreview: TReaderTapZoneContext['setShowPreview'];
|
||||
isSeriesMode?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<DefaultSettingFootnote areDefaultSettings={!isDefaultable} />
|
||||
{isSeriesMode && (
|
||||
<>
|
||||
<Typography>{t('reader.settings.source_series')}</Typography>
|
||||
<ReaderSettingReadingMode
|
||||
readingMode={settings.readingMode}
|
||||
setReadingMode={(value) => updateSetting('readingMode', value)}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('readingMode')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ReaderSettingPageGap
|
||||
pageGap={settings.pageGap}
|
||||
readingMode={settings.readingMode}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('pageGap')}
|
||||
updateSetting={(...args) => updateSetting('pageGap', ...args)}
|
||||
/>
|
||||
<ReaderSettingReadingDirection
|
||||
readingDirection={settings.readingDirection}
|
||||
setReadingDirection={(value) => updateSetting('readingDirection', value)}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('readingDirection')}
|
||||
/>
|
||||
<ReaderSettingTapZoneLayout
|
||||
tapZoneLayout={settings.tapZoneLayout}
|
||||
setTapZoneLayout={(value) => {
|
||||
setShowPreview(true);
|
||||
updateSetting('tapZoneLayout', value);
|
||||
}}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => {
|
||||
setShowPreview(true);
|
||||
onDefault?.('tapZoneLayout');
|
||||
}}
|
||||
/>
|
||||
<ReaderSettingTapZoneInvertMode
|
||||
tapZoneInvertMode={settings.tapZoneInvertMode}
|
||||
setTapZoneInvertMode={(value) => {
|
||||
setShowPreview(true);
|
||||
updateSetting('tapZoneInvertMode', value);
|
||||
}}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => {
|
||||
setShowPreview(true);
|
||||
onDefault?.('tapZoneInvertMode');
|
||||
}}
|
||||
/>
|
||||
<ReaderSettingPageScaleMode
|
||||
pageScaleMode={settings.pageScaleMode}
|
||||
setPageScaleMode={(value) => updateSetting('pageScaleMode', value)}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('pageScaleMode')}
|
||||
/>
|
||||
<ReaderSettingStretchPage
|
||||
pageScaleMode={settings.pageScaleMode.value}
|
||||
shouldStretchPage={settings.shouldStretchPage.value}
|
||||
setShouldStretchPage={(value) => updateSetting('shouldStretchPage', value)}
|
||||
/>
|
||||
<ReaderSettingWidth
|
||||
readerWidth={settings.readerWidth.value}
|
||||
pageScaleMode={settings.pageScaleMode.value}
|
||||
isDefaultable={isDefaultable}
|
||||
onDefault={() => onDefault?.('readerWidth')}
|
||||
updateSetting={(...args) => updateSetting(...args)}
|
||||
setTransparent={setTransparent}
|
||||
/>
|
||||
{isSeriesMode && (
|
||||
<>
|
||||
<Divider />
|
||||
<ReaderDefaultLayoutSettings
|
||||
profiles={[settings.readingMode.value]}
|
||||
updateSetting={updateSetting}
|
||||
isSeriesMode={isSeriesMode}
|
||||
setTransparent={setTransparent}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { IReaderSettings, IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/types/Reader.types.ts';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { DEFAULT_READER_SETTINGS, PAGE_GAP } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { isContinuousReadingMode } from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
|
||||
export const ReaderSettingPageGap = ({
|
||||
pageGap,
|
||||
readingMode,
|
||||
isDefaultable,
|
||||
onDefault,
|
||||
updateSetting,
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'pageGap' | 'readingMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<IReaderSettings['pageGap']>, 'isDefaultable' | 'onDefault'> & {
|
||||
updateSetting: (gap: number, commit: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isChangeable = readingMode.value !== ReadingMode.WEBTOON && isContinuousReadingMode(readingMode.value);
|
||||
if (!isChangeable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SliderInput
|
||||
label={t('reader.settings.label.page_gap')}
|
||||
value={t('global.value', { value: pageGap.value, unit: t('global.unit.px') })}
|
||||
onDefault={isDefaultable ? onDefault : undefined}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.pageGap,
|
||||
value: pageGap.value,
|
||||
step: PAGE_GAP.step,
|
||||
min: PAGE_GAP.min,
|
||||
max: PAGE_GAP.max,
|
||||
onChange: (_, value) => {
|
||||
updateSetting(value as number, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
updateSetting(value as number, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 {
|
||||
IReaderSettings,
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
ReadingDirection,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import {
|
||||
PAGE_SCALE_VALUE_TO_DISPLAY_DATA,
|
||||
READER_PAGE_SCALE_MODE_VALUES,
|
||||
} from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
|
||||
export const ReaderSettingPageScaleMode = ({
|
||||
pageScaleMode,
|
||||
setPageScaleMode,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'pageScaleMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
|
||||
setPageScaleMode: (mode: IReaderSettings['pageScaleMode']) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
{...buttonSelectInputProps}
|
||||
label={t('reader.settings.page_scale.title')}
|
||||
value={pageScaleMode.isDefault ? undefined : pageScaleMode.value}
|
||||
defaultValue={pageScaleMode.isDefault ? pageScaleMode.value : undefined}
|
||||
values={READER_PAGE_SCALE_MODE_VALUES}
|
||||
setValue={setPageScaleMode}
|
||||
valueToDisplayData={PAGE_SCALE_VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft';
|
||||
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/features/reader/types/Reader.types.ts';
|
||||
import { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = {
|
||||
[ReadingDirection.LTR]: {
|
||||
title: 'reader.settings.reading_direction.ltr',
|
||||
icon: <ArrowCircleRightIcon />,
|
||||
},
|
||||
[ReadingDirection.RTL]: {
|
||||
title: 'reader.settings.reading_direction.rtl',
|
||||
icon: <ArrowCircleLeftIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderSettingReadingDirection = ({
|
||||
readingDirection,
|
||||
setReadingDirection,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'readingDirection'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
|
||||
setReadingDirection: (readingDirection: ReadingDirection) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
{...buttonSelectInputProps}
|
||||
label={t('reader.settings.label.reading_direction')}
|
||||
value={readingDirection.isDefault ? undefined : readingDirection.value}
|
||||
defaultValue={readingDirection.isDefault ? readingDirection.value : undefined}
|
||||
values={READING_DIRECTION_VALUES}
|
||||
setValue={setReadingDirection}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/types/Reader.types.ts';
|
||||
import {
|
||||
READING_MODE_VALUE_TO_DISPLAY_DATA,
|
||||
READING_MODE_VALUES,
|
||||
} from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
|
||||
export const ReaderSettingReadingMode = ({
|
||||
readingMode,
|
||||
setReadingMode,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'readingMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingMode>, 'isDefaultable' | 'onDefault'> & {
|
||||
setReadingMode: (mode: ReadingMode) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
{...buttonSelectInputProps}
|
||||
label={t('reader.settings.label.reading_mode')}
|
||||
value={readingMode.isDefault ? undefined : readingMode.value}
|
||||
defaultValue={readingMode.isDefault ? readingMode.value : undefined}
|
||||
values={READING_MODE_VALUES}
|
||||
setValue={setReadingMode}
|
||||
valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 Box from '@mui/material/Box';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
|
||||
export const ReaderSettingStretchPage = ({
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
setShouldStretchPage,
|
||||
}: Pick<IReaderSettings, 'pageScaleMode' | 'shouldStretchPage'> & {
|
||||
setShouldStretchPage: (mode: IReaderSettings['shouldStretchPage']) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED[pageScaleMode]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Button
|
||||
onClick={() => setShouldStretchPage(!shouldStretchPage)}
|
||||
variant={shouldStretchPage ? 'contained' : 'outlined'}
|
||||
>
|
||||
{t('reader.settings.page_scale.stretch')}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
ReadingDirection,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { TapZoneInvertMode } from '@/features/reader/types/TapZoneLayout.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
enum TapZonesInvertOption {
|
||||
NONE,
|
||||
HORIZONTAL,
|
||||
VERTICAL,
|
||||
BOTH,
|
||||
}
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<TapZonesInvertOption> = {
|
||||
[TapZonesInvertOption.NONE]: {
|
||||
title: 'global.label.none',
|
||||
icon: null,
|
||||
},
|
||||
[TapZonesInvertOption.HORIZONTAL]: {
|
||||
title: 'global.label.horizontal',
|
||||
icon: null,
|
||||
},
|
||||
[TapZonesInvertOption.VERTICAL]: {
|
||||
title: 'global.label.vertical',
|
||||
icon: null,
|
||||
},
|
||||
[TapZonesInvertOption.BOTH]: {
|
||||
title: 'global.label.both',
|
||||
icon: null,
|
||||
},
|
||||
};
|
||||
|
||||
const TAP_ZONES_INVERT_OPTION_VALUES = Object.values(TapZonesInvertOption).filter((value) => typeof value === 'number');
|
||||
|
||||
const TAP_ZONES_INVERT_OPTION_TO_SETTING: Record<TapZonesInvertOption, TapZoneInvertMode> = {
|
||||
[TapZonesInvertOption.NONE]: {
|
||||
vertical: false,
|
||||
horizontal: false,
|
||||
},
|
||||
[TapZonesInvertOption.HORIZONTAL]: {
|
||||
vertical: false,
|
||||
horizontal: true,
|
||||
},
|
||||
[TapZonesInvertOption.VERTICAL]: {
|
||||
vertical: true,
|
||||
horizontal: false,
|
||||
},
|
||||
[TapZonesInvertOption.BOTH]: {
|
||||
vertical: true,
|
||||
horizontal: true,
|
||||
},
|
||||
};
|
||||
|
||||
const convertTapZoneInvertModeToOption = ({ vertical, horizontal }: TapZoneInvertMode): TapZonesInvertOption => {
|
||||
if (vertical && horizontal) {
|
||||
return TapZonesInvertOption.BOTH;
|
||||
}
|
||||
|
||||
if (vertical) {
|
||||
return TapZonesInvertOption.VERTICAL;
|
||||
}
|
||||
|
||||
if (horizontal) {
|
||||
return TapZonesInvertOption.HORIZONTAL;
|
||||
}
|
||||
|
||||
return TapZonesInvertOption.NONE;
|
||||
};
|
||||
|
||||
export const ReaderSettingTapZoneInvertMode = ({
|
||||
tapZoneInvertMode,
|
||||
setTapZoneInvertMode,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'tapZoneInvertMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
|
||||
setTapZoneInvertMode: (invert: IReaderSettings['tapZoneInvertMode']) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tapZonesInvertOption = convertTapZoneInvertModeToOption(tapZoneInvertMode.value);
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
{...buttonSelectInputProps}
|
||||
label={t('reader.settings.tap_zones.invert')}
|
||||
value={tapZoneInvertMode.isDefault ? undefined : tapZonesInvertOption}
|
||||
defaultValue={tapZoneInvertMode.isDefault ? tapZonesInvertOption : undefined}
|
||||
values={TAP_ZONES_INVERT_OPTION_VALUES}
|
||||
setValue={(value) => setTapZoneInvertMode(TAP_ZONES_INVERT_OPTION_TO_SETTING[value])}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { TapZoneLayouts } from '@/features/reader/types/TapZoneLayout.types.ts';
|
||||
import { MultiValueButtonDefaultableProps, ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<TapZoneLayouts> = {
|
||||
[TapZoneLayouts.EDGE]: {
|
||||
title: 'reader.settings.tap_zones.edge',
|
||||
icon: null,
|
||||
},
|
||||
[TapZoneLayouts.KINDLE]: {
|
||||
title: 'reader.settings.tap_zones.kindle',
|
||||
icon: null,
|
||||
},
|
||||
[TapZoneLayouts.L_SHAPE]: {
|
||||
title: 'reader.settings.tap_zones.l_shape',
|
||||
icon: null,
|
||||
},
|
||||
[TapZoneLayouts.RIGHT_LEFT]: {
|
||||
title: 'reader.settings.tap_zones.right_left',
|
||||
icon: null,
|
||||
},
|
||||
[TapZoneLayouts.DISABLED]: {
|
||||
title: 'global.label.disabled',
|
||||
icon: null,
|
||||
},
|
||||
};
|
||||
|
||||
const READER_TAP_ZONE_LAYOUT_VALUES = Object.values(TapZoneLayouts).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderSettingTapZoneLayout = ({
|
||||
tapZoneLayout,
|
||||
setTapZoneLayout,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'tapZoneLayout'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
|
||||
setTapZoneLayout: (layout: TapZoneLayouts) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
{...buttonSelectInputProps}
|
||||
label={t('reader.settings.tap_zones.title')}
|
||||
value={tapZoneLayout.isDefault ? undefined : tapZoneLayout.value}
|
||||
defaultValue={tapZoneLayout.isDefault ? tapZoneLayout.value : undefined}
|
||||
values={READER_TAP_ZONE_LAYOUT_VALUES}
|
||||
setValue={setTapZoneLayout}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/features/reader/types/Reader.types.ts';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { DEFAULT_READER_SETTINGS } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { isReaderWidthEditable } from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
import { ResetButton } from '@/features/core/components/buttons/ResetButton.tsx';
|
||||
|
||||
export const ReaderSettingWidth = ({
|
||||
readerWidth,
|
||||
pageScaleMode,
|
||||
isDefaultable,
|
||||
onDefault,
|
||||
updateSetting,
|
||||
setTransparent,
|
||||
}: Pick<IReaderSettings, 'readerWidth' | 'pageScaleMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<IReaderSettings['readerWidth']['value']>, 'isDefaultable' | 'onDefault'> & {
|
||||
updateSetting: (...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>) => void;
|
||||
setTransparent?: (transparent: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isReaderWidthEditable(pageScaleMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.label.limit_reader_width')}
|
||||
checked={readerWidth.enabled}
|
||||
onChange={(_, checked) => updateSetting('readerWidth', { ...readerWidth, enabled: checked }, true)}
|
||||
/>
|
||||
{isDefaultable && <ResetButton onClick={onDefault} variant="outlined" />}
|
||||
</Stack>
|
||||
{readerWidth.enabled && (
|
||||
<SliderInput
|
||||
label={t('reader.settings.label.reader_width')}
|
||||
value={t('global.value', { value: readerWidth.value, unit: '%' })}
|
||||
slotProps={{
|
||||
slider: {
|
||||
defaultValue: DEFAULT_READER_SETTINGS.readerWidth.value,
|
||||
value: readerWidth.value,
|
||||
step: 1,
|
||||
min: 10,
|
||||
max: 100,
|
||||
onChange: (_, value) => {
|
||||
setTransparent?.(true);
|
||||
updateSetting('readerWidth', { ...readerWidth, value: value as number }, false);
|
||||
},
|
||||
onChangeCommitted: (_, value) => {
|
||||
setTransparent?.(false);
|
||||
updateSetting('readerWidth', { ...readerWidth, value: value as number }, true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 Stack from '@mui/material/Stack';
|
||||
import ListSubheader from '@mui/material/ListSubheader';
|
||||
import { ComponentProps, useMemo } from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ReaderLayoutSettings } from '@/features/reader/components/settings/layout/ReaderLayoutSettings.tsx';
|
||||
import { useDefaultReaderSettingsWithDefaultFlag } from '@/features/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { ReadingMode } from '@/features/reader/types/Reader.types.ts';
|
||||
|
||||
export const ReaderSettingProfileSettings = ({
|
||||
profile,
|
||||
title,
|
||||
updateSetting,
|
||||
isSeriesMode,
|
||||
...props
|
||||
}: Pick<ComponentProps<typeof ReaderLayoutSettings>, 'updateSetting' | 'isSeriesMode'> & {
|
||||
profile: ReadingMode;
|
||||
title: string;
|
||||
}) => {
|
||||
const { settings } = useDefaultReaderSettingsWithDefaultFlag(profile);
|
||||
|
||||
const adjustedSettings = useMemo(
|
||||
() => ({
|
||||
...settings,
|
||||
readingMode: {
|
||||
value: profile,
|
||||
isDefault: true,
|
||||
},
|
||||
}),
|
||||
[settings, profile],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
{!isSeriesMode ? (
|
||||
<ListSubheader component="div" id={`${profile}-settings`}>
|
||||
{title}
|
||||
</ListSubheader>
|
||||
) : (
|
||||
<Typography>{title}</Typography>
|
||||
)}
|
||||
<Stack sx={{ px: Number(!isSeriesMode) * 2 }}>
|
||||
<ReaderLayoutSettings
|
||||
{...props}
|
||||
setShowPreview={() => {}}
|
||||
settings={adjustedSettings}
|
||||
updateSetting={(setting, value, commit) => updateSetting(setting, value, commit, true, profile)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
469
src/features/reader/components/viewer/ReaderChapterViewer.tsx
Normal file
469
src/features/reader/components/viewer/ReaderChapterViewer.tsx
Normal file
@@ -0,0 +1,469 @@
|
||||
/*
|
||||
* 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, MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderPagerProps,
|
||||
ReaderPageSpreadState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReaderTransitionPageMode,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { getDoublePageModePages } from '@/features/reader/utils/ReaderPager.utils.tsx';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import {
|
||||
getPagerForReadingMode,
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
shouldApplyReaderWidth,
|
||||
} from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import {
|
||||
createHandleReaderPageLoadError,
|
||||
createUpdateReaderPageLoadState,
|
||||
} from '@/features/reader/utils/Reader.utils.ts';
|
||||
import { useReaderConvertPagesForReadingMode } from '@/features/reader/hooks/useReaderConvertPagesForReadingMode.ts';
|
||||
import { ReaderTransitionPage } from '@/features/reader/components/viewer/ReaderTransitionPage.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { READER_STATE_PAGES_DEFAULTS } from '@/features/reader/constants/ReaderContext.constants.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { useReaderSetPagesState } from '@/features/reader/hooks/useReaderSetPagesState.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/features/core/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
||||
import { getErrorMessage, noOp } from '@/lib/HelperFunctions.ts';
|
||||
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { ReaderInfiniteScrollUpdateChapter } from '@/features/reader/components/viewer/ReaderInfiniteScrollUpdateChapter.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
const BaseReaderChapterViewer = ({
|
||||
currentPageIndex,
|
||||
setPages: setContextPages,
|
||||
setPageLoadStates: setContextPageLoadStates,
|
||||
setTotalPages: setContextTotalPages,
|
||||
setCurrentPageIndex: setContextCurrentPageIndex,
|
||||
updateCurrentPageIndex,
|
||||
setPageToScrollToIndex,
|
||||
transitionPageMode,
|
||||
retryFailedPagesKeyPrefix,
|
||||
setTransitionPageMode,
|
||||
readingMode,
|
||||
readerWidth,
|
||||
pageScaleMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
readingDirection,
|
||||
shouldUseInfiniteScroll,
|
||||
imagePreLoadAmount,
|
||||
pageGap,
|
||||
chapterId,
|
||||
previousChapterId,
|
||||
nextChapterId,
|
||||
isPreviousChapterVisible,
|
||||
isNextChapterVisible,
|
||||
lastPageRead,
|
||||
isInitialChapter,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
isLeadingChapter,
|
||||
isTrailingChapter,
|
||||
isPreloadMode,
|
||||
imageRefs: globalImageRefs,
|
||||
scrollIntoView,
|
||||
setReaderStateChapters,
|
||||
resumeMode,
|
||||
customFilter,
|
||||
shouldStretchPage,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
readerNavBarWidth,
|
||||
onSizeChange,
|
||||
minWidth,
|
||||
minHeight,
|
||||
scrollElement,
|
||||
}: Pick<
|
||||
ReaderStatePages,
|
||||
| 'currentPageIndex'
|
||||
| 'setPages'
|
||||
| 'setPageLoadStates'
|
||||
| 'setTotalPages'
|
||||
| 'setCurrentPageIndex'
|
||||
| 'setPageToScrollToIndex'
|
||||
| 'transitionPageMode'
|
||||
| 'retryFailedPagesKeyPrefix'
|
||||
| 'setTransitionPageMode'
|
||||
> &
|
||||
Omit<ReaderPagerProps, 'pages' | 'totalPages' | 'pageLoadStates' | 'handleAsInitialRender' | 'resumeMode'> &
|
||||
Pick<
|
||||
IReaderSettings,
|
||||
| 'readingMode'
|
||||
| 'shouldOffsetDoubleSpreads'
|
||||
| 'readingDirection'
|
||||
| 'readerWidth'
|
||||
| 'pageScaleMode'
|
||||
| 'shouldUseInfiniteScroll'
|
||||
> &
|
||||
Pick<ReaderStateChapters, 'setReaderStateChapters'> & {
|
||||
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
|
||||
chapterId: ChapterIdInfo['id'];
|
||||
previousChapterId?: ChapterIdInfo['id'];
|
||||
nextChapterId?: ChapterIdInfo['id'];
|
||||
isPreviousChapterVisible: boolean;
|
||||
isNextChapterVisible: boolean;
|
||||
lastPageRead: number;
|
||||
isInitialChapter: boolean;
|
||||
isCurrentChapter: boolean;
|
||||
isPreviousChapter: boolean;
|
||||
isNextChapter: boolean;
|
||||
isLeadingChapter: boolean;
|
||||
isTrailingChapter: boolean;
|
||||
imageRefs: MutableRefObject<(HTMLElement | null)[]>;
|
||||
scrollIntoView: boolean;
|
||||
resumeMode: ReaderResumeMode;
|
||||
onSizeChange: (width: number, height: number, chapterId: ChapterIdInfo['id']) => void;
|
||||
minWidth: number;
|
||||
minHeight: number;
|
||||
scrollElement: HTMLElement | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { direction: themeDirection } = useTheme();
|
||||
|
||||
const [fetchPages, pagesResponse] = requestManager.useGetChapterPagesFetch(chapterId ?? -1);
|
||||
|
||||
const [arePagesFetched, setArePagesFetched] = useState(false);
|
||||
const [totalPages, setTotalPages] = useState<ReaderStatePages['totalPages']>(
|
||||
READER_STATE_PAGES_DEFAULTS.totalPages,
|
||||
);
|
||||
const [pageUrls, setPageUrls] = useState<ReaderStatePages['pageUrls']>(READER_STATE_PAGES_DEFAULTS.pageUrls);
|
||||
const [pages, setPages] = useState<ReaderStatePages['pages']>(READER_STATE_PAGES_DEFAULTS.pages);
|
||||
const [pageLoadStates, setPageLoadStates] = useState<ReaderStatePages['pageLoadStates']>(
|
||||
READER_STATE_PAGES_DEFAULTS.pageLoadStates,
|
||||
);
|
||||
const [pagesToSpreadState, setPagesToSpreadState] = useState<ReaderPageSpreadState[]>(
|
||||
pageLoadStates.map(({ url }) => ({ url, isSpread: false })),
|
||||
);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isCurrentChapterRef = useRef(isCurrentChapter);
|
||||
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
|
||||
const pagerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const actualPages = useMemo(() => {
|
||||
const arePagesLoaded = !!totalPages;
|
||||
if (!arePagesLoaded) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
const isSpreadStateUpdated = totalPages === pagesToSpreadState.length;
|
||||
if (!isSpreadStateUpdated) {
|
||||
return pages;
|
||||
}
|
||||
|
||||
if (readingMode === ReadingMode.DOUBLE_PAGE) {
|
||||
return getDoublePageModePages(pageUrls, pagesToSpreadState, shouldOffsetDoubleSpreads, readingDirection);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}, [pagesToSpreadState, readingMode, shouldOffsetDoubleSpreads, readingDirection, totalPages]);
|
||||
|
||||
const Pager = useMemo(() => getPagerForReadingMode(readingMode), [readingMode]);
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||
const shouldHideChapter = (!isContinuousReadingModeActive && !isCurrentChapter) || isPreloadMode;
|
||||
|
||||
const isCurrentChapterInSinglePager = !isContinuousReadingModeActive && isCurrentChapter;
|
||||
const showPreviousTransitionPage =
|
||||
!shouldHideChapter &&
|
||||
(isCurrentChapterInSinglePager || (isContinuousReadingModeActive && (isInitialChapter || isLeadingChapter)));
|
||||
const showNextTransitionPage =
|
||||
!shouldHideChapter &&
|
||||
(isCurrentChapterInSinglePager || (isContinuousReadingModeActive && (isInitialChapter || isTrailingChapter)));
|
||||
|
||||
isCurrentChapterRef.current = isCurrentChapter;
|
||||
if (isCurrentChapter) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
globalImageRefs.current = imageRefs.current;
|
||||
}
|
||||
|
||||
const doFetchPages = useCallback(() => {
|
||||
if (!chapterId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setArePagesFetched(false);
|
||||
|
||||
fetchPages({ variables: { input: { chapterId } } }).catch(
|
||||
defaultPromiseErrorHandler(`ReaderChapterViewer(${chapterId})::fetchPages`),
|
||||
);
|
||||
}, [fetchPages, chapterId]);
|
||||
|
||||
const onLoad = useMemo(
|
||||
() =>
|
||||
createUpdateReaderPageLoadState(
|
||||
actualPages,
|
||||
setPagesToSpreadState,
|
||||
(value) => {
|
||||
if (isCurrentChapterRef.current) {
|
||||
setContextPageLoadStates(value);
|
||||
}
|
||||
|
||||
setPageLoadStates(value);
|
||||
},
|
||||
readingMode,
|
||||
),
|
||||
[actualPages, readingMode],
|
||||
);
|
||||
|
||||
const onError = useMemo(
|
||||
() =>
|
||||
createHandleReaderPageLoadError((value) => {
|
||||
if (isCurrentChapterRef.current) {
|
||||
setContextPageLoadStates(value);
|
||||
}
|
||||
|
||||
setPageLoadStates(value);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
doFetchPages();
|
||||
}, [chapterId]);
|
||||
|
||||
useResizeObserver(
|
||||
ref,
|
||||
useCallback(
|
||||
(entries) => {
|
||||
const { clientWidth, clientHeight } = entries[0].target;
|
||||
onSizeChange(clientWidth, clientHeight, chapterId);
|
||||
},
|
||||
[onSizeChange, ref.current, chapterId],
|
||||
),
|
||||
);
|
||||
|
||||
const updateState = <T,>(
|
||||
value: T,
|
||||
setLocalState: (value: T) => void,
|
||||
setGlobalState: (value: T) => void,
|
||||
forceLocal: boolean = false,
|
||||
) => {
|
||||
if (forceLocal || !arePagesFetched) {
|
||||
setLocalState(value);
|
||||
}
|
||||
|
||||
if (isCurrentChapter) {
|
||||
setGlobalState(value);
|
||||
}
|
||||
};
|
||||
useReaderSetPagesState(
|
||||
isCurrentChapter,
|
||||
pagesResponse,
|
||||
resumeMode,
|
||||
lastPageRead,
|
||||
actualPages,
|
||||
pageLoadStates,
|
||||
pagesToSpreadState,
|
||||
arePagesFetched,
|
||||
setArePagesFetched,
|
||||
(value) => updateState(value, noOp, setReaderStateChapters),
|
||||
(value) => updateState(value, setTotalPages, setContextTotalPages),
|
||||
(value) => updateState(value, setPages, setContextPages),
|
||||
(value) => updateState(value, setPageUrls, noOp),
|
||||
(value) => updateState(value, setPageLoadStates, setContextPageLoadStates),
|
||||
(value) => updateState(value, setPagesToSpreadState, noOp),
|
||||
(value) => updateState(value, noOp, setContextCurrentPageIndex),
|
||||
(value) => {
|
||||
if ((isInitialChapter && !arePagesFetched) || scrollIntoView) {
|
||||
setPageToScrollToIndex(value);
|
||||
setReaderStateChapters((prevState) => ({
|
||||
...prevState,
|
||||
visibleChapters: { ...prevState.visibleChapters, scrollIntoView: false, resumeMode: undefined },
|
||||
}));
|
||||
}
|
||||
},
|
||||
(value) => updateState(value, noOp, setTransitionPageMode),
|
||||
);
|
||||
|
||||
useReaderConvertPagesForReadingMode(
|
||||
currentPageIndex,
|
||||
actualPages,
|
||||
pageUrls,
|
||||
(value) => updateState(value, setPages, setContextPages, true),
|
||||
(value) => updateState(value, setPagesToSpreadState, noOp, true),
|
||||
(value) => updateState(value, noOp, updateCurrentPageIndex),
|
||||
readingMode,
|
||||
);
|
||||
|
||||
// for non-continuous reading modes, only the current, previous and next chapter are relevant
|
||||
// every other chapter does not need to be rendered all the time since it's not affecting the
|
||||
// visible content anyway
|
||||
// the previous and next chapter are rendered so that going to the previous/next chapter feels smoother
|
||||
// since the relevant pages are already rendered
|
||||
const shouldRenderChapterViewer =
|
||||
isContinuousReadingModeActive || isCurrentChapter || isPreviousChapter || isNextChapter;
|
||||
if (!shouldRenderChapterViewer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pagesResponse.error) {
|
||||
if (shouldHideChapter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100%',
|
||||
minWidth: '100%',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
messageExtra={getErrorMessage(pagesResponse.error)}
|
||||
retry={() => {
|
||||
doFetchPages();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (pagesResponse.loading || !arePagesFetched) {
|
||||
if (shouldHideChapter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100%',
|
||||
minWidth: '100%',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
<LoadingPlaceholder />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (chapterId != null && !totalPages) {
|
||||
if (shouldHideChapter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ minWidth: '100%', minHeight: '100%', position: 'relative' }}>
|
||||
<EmptyViewAbsoluteCentered message={t('reader.error.label.no_pages_found')} retry={doFetchPages} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
ref={ref}
|
||||
sx={{
|
||||
width: 'fit-content',
|
||||
height: 'fit-content',
|
||||
margin: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
minHeight,
|
||||
}),
|
||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||
minWidth,
|
||||
}),
|
||||
...applyStyles(shouldHideChapter, {
|
||||
maxWidth: 0,
|
||||
maxHeight: 0,
|
||||
minWidth: 'unset',
|
||||
minHeight: 'unset',
|
||||
overflow: 'hidden',
|
||||
margin: 'unset',
|
||||
}),
|
||||
...applyStyles(
|
||||
isContinuousVerticalReadingMode(readingMode) && shouldApplyReaderWidth(readerWidth, pageScaleMode),
|
||||
{ alignItems: 'center' },
|
||||
),
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
...applyStyles(themeDirection === 'ltr', {
|
||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||
}),
|
||||
...applyStyles(themeDirection === 'rtl', {
|
||||
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{!isPreloadMode && (
|
||||
<ReaderInfiniteScrollUpdateChapter
|
||||
readingMode={readingMode}
|
||||
readingDirection={readingDirection}
|
||||
shouldUseInfiniteScroll={shouldUseInfiniteScroll}
|
||||
chapterId={chapterId}
|
||||
previousChapterId={previousChapterId}
|
||||
nextChapterId={nextChapterId}
|
||||
isCurrentChapter={isCurrentChapter}
|
||||
isPreviousChapterVisible={isPreviousChapterVisible}
|
||||
isNextChapterVisible={isNextChapterVisible}
|
||||
imageWrapper={pagerRef.current}
|
||||
scrollbarXSize={scrollbarXSize}
|
||||
scrollbarYSize={scrollbarYSize}
|
||||
scrollElement={scrollElement}
|
||||
/>
|
||||
)}
|
||||
{showPreviousTransitionPage && (
|
||||
<ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.PREVIOUS} />
|
||||
)}
|
||||
<Pager
|
||||
ref={pagerRef}
|
||||
totalPages={totalPages}
|
||||
currentPageIndex={currentPageIndex}
|
||||
pages={actualPages}
|
||||
transitionPageMode={transitionPageMode}
|
||||
pageLoadStates={pageLoadStates}
|
||||
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
|
||||
imageRefs={imageRefs}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
isCurrentChapter={isCurrentChapter}
|
||||
isPreviousChapter={isPreviousChapter}
|
||||
isNextChapter={isNextChapter}
|
||||
readingMode={readingMode}
|
||||
imagePreLoadAmount={imagePreLoadAmount}
|
||||
readingDirection={readingDirection}
|
||||
pageScaleMode={pageScaleMode}
|
||||
pageGap={pageGap}
|
||||
customFilter={customFilter}
|
||||
shouldStretchPage={shouldStretchPage}
|
||||
readerWidth={readerWidth}
|
||||
scrollbarXSize={scrollbarXSize}
|
||||
scrollbarYSize={scrollbarYSize}
|
||||
readerNavBarWidth={readerNavBarWidth}
|
||||
isPreloadMode={isPreloadMode}
|
||||
resumeMode={resumeMode}
|
||||
handleAsInitialRender={scrollIntoView}
|
||||
/>
|
||||
{showNextTransitionPage && (
|
||||
<ReaderTransitionPage chapterId={chapterId} type={ReaderTransitionPageMode.NEXT} />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderChapterViewer = memo(BaseReaderChapterViewer);
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 } from 'react';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderInfiniteScrollUpdateChapter } from '@/features/reader/hooks/useReaderInfiniteScrollUpdateChapter.ts';
|
||||
import { IReaderSettings, TReaderScrollbarContext } from '@/features/reader/types/Reader.types.ts';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
const BaseReaderInfiniteScrollUpdateChapter = ({
|
||||
readingMode,
|
||||
readingDirection,
|
||||
shouldUseInfiniteScroll,
|
||||
chapterId,
|
||||
previousChapterId,
|
||||
nextChapterId,
|
||||
isPreviousChapterVisible,
|
||||
isCurrentChapter,
|
||||
isNextChapterVisible,
|
||||
imageWrapper,
|
||||
openChapter,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
scrollElement,
|
||||
shouldShowTransitionPage,
|
||||
}: Pick<IReaderSettings, 'shouldShowTransitionPage'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> &
|
||||
Pick<IReaderSettings, 'readingMode' | 'readingDirection' | 'shouldUseInfiniteScroll'> & {
|
||||
chapterId: ChapterIdInfo['id'];
|
||||
previousChapterId?: ChapterIdInfo['id'];
|
||||
nextChapterId?: ChapterIdInfo['id'];
|
||||
isPreviousChapterVisible: boolean;
|
||||
isCurrentChapter: boolean;
|
||||
isNextChapterVisible: boolean;
|
||||
imageWrapper: HTMLElement | null;
|
||||
openChapter: ReturnType<typeof ReaderControls.useOpenChapter>;
|
||||
scrollElement: HTMLElement | null;
|
||||
}) => {
|
||||
useReaderInfiniteScrollUpdateChapter(
|
||||
'first',
|
||||
chapterId,
|
||||
previousChapterId,
|
||||
isCurrentChapter,
|
||||
isPreviousChapterVisible,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
shouldUseInfiniteScroll,
|
||||
openChapter,
|
||||
imageWrapper,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
scrollElement,
|
||||
shouldShowTransitionPage,
|
||||
);
|
||||
useReaderInfiniteScrollUpdateChapter(
|
||||
'last',
|
||||
chapterId,
|
||||
nextChapterId,
|
||||
isCurrentChapter,
|
||||
isNextChapterVisible,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
shouldUseInfiniteScroll,
|
||||
openChapter,
|
||||
imageWrapper,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
scrollElement,
|
||||
shouldShowTransitionPage,
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const ReaderInfiniteScrollUpdateChapter = withPropsFrom(
|
||||
memo(BaseReaderInfiniteScrollUpdateChapter),
|
||||
[() => ({ openChapter: ReaderControls.useOpenChapter() }), ReaderService.useSettingsWithoutDefaultFlag],
|
||||
['openChapter', 'shouldShowTransitionPage'],
|
||||
);
|
||||
170
src/features/reader/components/viewer/ReaderPage.tsx
Normal file
170
src/features/reader/components/viewer/ReaderPage.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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, useCallback } from 'react';
|
||||
import { SpinnerImage, SpinnerImageProps } from '@/features/core/components/SpinnerImage.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderCustomFilter,
|
||||
ReaderPagerProps,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import {
|
||||
getImageMarginStyling,
|
||||
getImagePlaceholderStyling,
|
||||
getReaderImageStyling,
|
||||
} from '@/features/reader/utils/ReaderPager.utils.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { MediaQuery } from '@/features/core/utils/MediaQuery.tsx';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
|
||||
const getCustomFilterString = (customFilter: ReaderCustomFilter): string =>
|
||||
Object.keys(customFilter)
|
||||
.map((key) => {
|
||||
const filter = key as keyof ReaderCustomFilter;
|
||||
const value = customFilter[filter];
|
||||
|
||||
switch (filter) {
|
||||
case 'brightness':
|
||||
case 'contrast':
|
||||
case 'saturate':
|
||||
return (value as ReaderCustomFilter['brightness' | 'contrast' | 'saturate']).enabled
|
||||
? `${filter}(${(value as ReaderCustomFilter['brightness' | 'contrast' | 'saturate']).value / 100})`
|
||||
: '';
|
||||
case 'hue':
|
||||
return (value as ReaderCustomFilter['hue']).enabled
|
||||
? `hue-rotate(${(value as ReaderCustomFilter['brightness' | 'contrast' | 'saturate']).value}deg)`
|
||||
: '';
|
||||
case 'rgba':
|
||||
return '';
|
||||
case 'sepia':
|
||||
case 'grayscale':
|
||||
case 'invert':
|
||||
return value ? `${filter}(${Number(value)})` : '';
|
||||
default:
|
||||
throw new Error(`Unexpected "CustomFilter" (${filter})`);
|
||||
}
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
const BaseReaderPage = ({
|
||||
pageIndex,
|
||||
pagesIndex,
|
||||
isPrimaryPage,
|
||||
display,
|
||||
doublePage = false,
|
||||
position,
|
||||
marginTop,
|
||||
shouldLoad,
|
||||
readingMode,
|
||||
customFilter,
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
readerWidth,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
onLoad,
|
||||
onError,
|
||||
setRef,
|
||||
readerNavBarWidth,
|
||||
isLoaded,
|
||||
...props
|
||||
}: Omit<SpinnerImageProps, 'spinnerStyle' | 'imgStyle' | 'onLoad' | 'onError'> &
|
||||
Pick<IReaderSettings, 'readingMode' | 'customFilter' | 'pageScaleMode' | 'shouldStretchPage' | 'readerWidth'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> &
|
||||
Pick<NavbarContextType, 'readerNavBarWidth'> & {
|
||||
pageIndex: number;
|
||||
pagesIndex: number;
|
||||
isPrimaryPage: boolean;
|
||||
display: boolean;
|
||||
doublePage?: boolean;
|
||||
position?: 'left' | 'right';
|
||||
marginTop?: number;
|
||||
onLoad: ReaderPagerProps['onLoad'];
|
||||
onError: ReaderPagerProps['onError'];
|
||||
setRef?: (pagesIndex: number, ref: HTMLElement | null) => void;
|
||||
isLoaded?: boolean;
|
||||
}) => {
|
||||
const { src } = props;
|
||||
|
||||
const isTabletWidth = MediaQuery.useIsTabletWidth();
|
||||
|
||||
const handleLoad = useCallback(
|
||||
() => onLoad?.(pagesIndex, src, isPrimaryPage),
|
||||
[onLoad, pagesIndex, src, isPrimaryPage],
|
||||
);
|
||||
const handleError = useCallback(() => onError?.(pageIndex, src), [onError, pageIndex, src]);
|
||||
const updateRef = useCallback((element: HTMLElement | null) => setRef?.(pagesIndex, element), [pagesIndex, setRef]);
|
||||
|
||||
if (!display && !shouldLoad) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SpinnerImage
|
||||
{...props}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
shouldLoad={shouldLoad}
|
||||
shouldDecode
|
||||
ref={updateRef}
|
||||
spinnerStyle={{
|
||||
backgroundColor: 'background.paper',
|
||||
...getImagePlaceholderStyling(
|
||||
readingMode,
|
||||
shouldStretchPage,
|
||||
pageScaleMode,
|
||||
readerWidth,
|
||||
readerNavBarWidth + scrollbarYSize,
|
||||
scrollbarXSize,
|
||||
doublePage,
|
||||
isTabletWidth,
|
||||
),
|
||||
...applyStyles(!display, {
|
||||
display: 'none',
|
||||
}),
|
||||
...getImageMarginStyling(doublePage, position),
|
||||
}}
|
||||
imgStyle={{
|
||||
...getReaderImageStyling(
|
||||
readingMode,
|
||||
shouldStretchPage,
|
||||
pageScaleMode,
|
||||
doublePage,
|
||||
readerWidth,
|
||||
readerNavBarWidth + scrollbarYSize,
|
||||
scrollbarXSize,
|
||||
),
|
||||
filter: getCustomFilterString(customFilter),
|
||||
objectFit: 'contain',
|
||||
objectPosition: position,
|
||||
userSelect: 'none',
|
||||
...getImageMarginStyling(doublePage, position),
|
||||
...applyStyles(marginTop !== undefined, {
|
||||
mt: `${marginTop}px`,
|
||||
}),
|
||||
display: 'block',
|
||||
...applyStyles(!display, {
|
||||
display: 'none',
|
||||
}),
|
||||
...applyStyles(!isLoaded, {
|
||||
margin: 'unset',
|
||||
}),
|
||||
}}
|
||||
hideImgStyle={{
|
||||
visibility: 'hidden',
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderPage = memo(BaseReaderPage);
|
||||
305
src/features/reader/components/viewer/ReaderTransitionPage.tsx
Normal file
305
src/features/reader/components/viewer/ReaderTransitionPage.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* 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 Typography from '@mui/material/Typography';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ComponentProps, memo, useMemo } from 'react';
|
||||
import { alpha, useTheme } from '@mui/material/styles';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderTransitionPageMode,
|
||||
ReadingMode,
|
||||
TReaderScrollbarContext,
|
||||
TReaderStateMangaContext,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { isTransitionPageVisible } from '@/features/reader/utils/ReaderPager.utils.tsx';
|
||||
import { useBackButton } from '@/features/core/hooks/useBackButton.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import {
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
} from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { AppRoutes } from '@/features/core/AppRoute.constants.ts';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { getValueFromObject, noOp } from '@/lib/HelperFunctions.ts';
|
||||
import { READER_BACKGROUND_TO_COLOR } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderStatePages } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
const ChapterInfo = ({
|
||||
title,
|
||||
name,
|
||||
scanlator,
|
||||
backgroundColor,
|
||||
}: {
|
||||
title: string;
|
||||
name?: ChapterType['name'];
|
||||
scanlator?: ChapterType['scanlator'];
|
||||
backgroundColor: IReaderSettings['backgroundColor'];
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const contrastText = theme.palette.getContrastText(
|
||||
getValueFromObject(theme.palette, READER_BACKGROUND_TO_COLOR[backgroundColor]),
|
||||
);
|
||||
const disabledText = alpha(contrastText, 0.5);
|
||||
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Typography color={contrastText}>{title}</Typography>
|
||||
<Typography color={contrastText} variant="h6" component="h1">
|
||||
{name}
|
||||
</Typography>
|
||||
{scanlator && (
|
||||
<Typography variant="body2" color={disabledText}>
|
||||
{scanlator}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const BaseReaderTransitionPage = ({
|
||||
type,
|
||||
transitionPageMode,
|
||||
readingMode,
|
||||
backgroundColor,
|
||||
shouldShowTransitionPage,
|
||||
manga,
|
||||
currentChapterName,
|
||||
currentChapterScanlator,
|
||||
previousChapterName,
|
||||
previousChapterScanlator,
|
||||
nextChapterName,
|
||||
nextChapterScanlator,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
readerNavBarWidth,
|
||||
handleBack,
|
||||
}: Pick<IReaderSettings, 'readingMode' | 'backgroundColor' | 'shouldShowTransitionPage'> &
|
||||
Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> &
|
||||
Pick<ReaderStatePages, 'transitionPageMode'> &
|
||||
Pick<NavbarContextType, 'readerNavBarWidth'> & {
|
||||
// gets used in the "source props creators" of the "withPropsFrom" call
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
chapterId: ChapterIdInfo['id'];
|
||||
currentChapterName?: ChapterType['name'];
|
||||
currentChapterScanlator?: ChapterType['scanlator'];
|
||||
previousChapterName?: ChapterType['name'];
|
||||
previousChapterScanlator?: ChapterType['scanlator'];
|
||||
nextChapterName?: ChapterType['name'];
|
||||
nextChapterScanlator?: ChapterType['scanlator'];
|
||||
type: Exclude<ReaderTransitionPageMode, ReaderTransitionPageMode.NONE | ReaderTransitionPageMode.BOTH>;
|
||||
handleBack: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isPreviousType = type === ReaderTransitionPageMode.PREVIOUS;
|
||||
const isNextType = type === ReaderTransitionPageMode.NEXT;
|
||||
|
||||
const isFirstChapter = !!currentChapterName && !previousChapterName;
|
||||
const isLastChapter = !!currentChapterName && !nextChapterName;
|
||||
|
||||
const forceShowFirstChapterPreviousTransitionPage = isFirstChapter && type === ReaderTransitionPageMode.PREVIOUS;
|
||||
const forceShowLastChapterNextTransitionPage = isLastChapter && type === ReaderTransitionPageMode.NEXT;
|
||||
const forceShowTransitionPage =
|
||||
forceShowFirstChapterPreviousTransitionPage || forceShowLastChapterNextTransitionPage;
|
||||
|
||||
if (!shouldShowTransitionPage && !forceShowTransitionPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isTransitionPageVisible(type, transitionPageMode, readingMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
...applyStyles(!isContinuousReadingMode(readingMode), {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}),
|
||||
...applyStyles(isContinuousReadingMode(readingMode), {
|
||||
position: 'sticky',
|
||||
...applyStyles(isContinuousVerticalReadingMode(readingMode), {
|
||||
left: 0,
|
||||
maxWidth: `calc(100vw - ${scrollbarYSize}px - ${readerNavBarWidth}px)`,
|
||||
minHeight: `calc(100vh - ${scrollbarXSize}px)`,
|
||||
}),
|
||||
...applyStyles(readingMode === ReadingMode.CONTINUOUS_HORIZONTAL, {
|
||||
top: 0,
|
||||
minWidth: `calc(100vw - ${scrollbarYSize}px - ${readerNavBarWidth}px)`,
|
||||
maxHeight: `calc(100vh - ${scrollbarXSize}px)`,
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
gap: 2,
|
||||
maxWidth: (theme) =>
|
||||
// spacing = added padding left + right
|
||||
`calc(100vw - ${scrollbarYSize}px - ${readerNavBarWidth}px - ${theme.spacing(2)})`,
|
||||
maxHeight: `calc(100vh - ${scrollbarXSize}px)`,
|
||||
width: 'max-content',
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
{isPreviousType && isFirstChapter && (
|
||||
<Typography variant="h6">{t('reader.transition_page.first_chapter')}</Typography>
|
||||
)}
|
||||
<Stack sx={{ gap: 5 }}>
|
||||
{isPreviousType && !isFirstChapter && (
|
||||
<ChapterInfo
|
||||
title={t('reader.transition_page.previous')}
|
||||
name={previousChapterName}
|
||||
scanlator={previousChapterScanlator}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
)}
|
||||
{!!currentChapterName && (
|
||||
<ChapterInfo
|
||||
title={t(
|
||||
isPreviousType ? 'reader.transition_page.current' : 'reader.transition_page.finished',
|
||||
)}
|
||||
name={currentChapterName}
|
||||
scanlator={currentChapterScanlator}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
)}
|
||||
{isNextType && !isLastChapter && (
|
||||
<ChapterInfo
|
||||
title={t('reader.transition_page.next')}
|
||||
name={nextChapterName}
|
||||
scanlator={nextChapterScanlator}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
{isNextType && isLastChapter && (
|
||||
<Typography variant="h6">{t('reader.transition_page.last_chapter')}</Typography>
|
||||
)}
|
||||
{((isPreviousType && isFirstChapter) || (isNextType && isLastChapter)) && (
|
||||
<Stack sx={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
|
||||
<Button
|
||||
sx={{ flexGrow: 1 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleBack();
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
{t('reader.transition_page.exit.previous_page')}
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ flexGrow: 1 }}
|
||||
component={Link}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
variant="contained"
|
||||
to={AppRoutes.manga.path(manga?.id ?? -1)}
|
||||
>
|
||||
{t('reader.transition_page.exit.manga_page')}
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderTransitionPage = withPropsFrom(
|
||||
memo(BaseReaderTransitionPage) as typeof BaseReaderTransitionPage,
|
||||
[
|
||||
useReaderStateMangaContext,
|
||||
({ chapterId }: Pick<ComponentProps<typeof BaseReaderTransitionPage>, 'chapterId'>) => {
|
||||
const { chapters } = useReaderStateChaptersContext();
|
||||
|
||||
const currentChapterIndex = useMemo(
|
||||
() => chapters.findIndex((chapter) => chapter.id === chapterId),
|
||||
[chapterId, chapters],
|
||||
);
|
||||
const currentChapter = chapters[currentChapterIndex];
|
||||
// chapters are sorted from latest to oldest
|
||||
const previousChapter = useMemo(() => chapters[currentChapterIndex + 1], [currentChapterIndex, chapters]);
|
||||
const nextChapter = useMemo(() => chapters[currentChapterIndex - 1], [currentChapterIndex, chapters]);
|
||||
|
||||
return {
|
||||
currentChapterName: currentChapter?.name,
|
||||
currentChapterScanlator: currentChapter?.scanlator,
|
||||
previousChapterName: previousChapter?.name,
|
||||
previousChapterScanlator: previousChapter?.name,
|
||||
nextChapterName: nextChapter?.name,
|
||||
nextChapterScanlator: nextChapter?.scanlator,
|
||||
};
|
||||
},
|
||||
useReaderScrollbarContext,
|
||||
useNavBarContext,
|
||||
userReaderStatePagesContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
({ chapterId, type }: Pick<ComponentProps<typeof BaseReaderTransitionPage>, 'chapterId' | 'type'>) => {
|
||||
const handleBack = useBackButton();
|
||||
const { chapters } = useReaderStateChaptersContext();
|
||||
|
||||
const currentChapterIndex = useMemo(
|
||||
() => chapters.findIndex((chapter) => chapter.id === chapterId),
|
||||
[chapterId, chapters],
|
||||
);
|
||||
|
||||
// chapters are sorted from latest to oldest
|
||||
const isLastChapter = currentChapterIndex === 0;
|
||||
const isFirstChapter = currentChapterIndex === chapters.length - 1;
|
||||
|
||||
const handleBackFirstChapter = type === ReaderTransitionPageMode.PREVIOUS && isFirstChapter;
|
||||
const handleBackLastChapter = type === ReaderTransitionPageMode.NEXT && isLastChapter;
|
||||
|
||||
const needsToHandleBack = handleBackFirstChapter || handleBackLastChapter;
|
||||
|
||||
return {
|
||||
handleBack: needsToHandleBack ? handleBack : noOp,
|
||||
};
|
||||
},
|
||||
],
|
||||
[
|
||||
'manga',
|
||||
'currentChapterName',
|
||||
'currentChapterScanlator',
|
||||
'previousChapterName',
|
||||
'previousChapterScanlator',
|
||||
'nextChapterName',
|
||||
'nextChapterScanlator',
|
||||
'scrollbarXSize',
|
||||
'scrollbarYSize',
|
||||
'readerNavBarWidth',
|
||||
'backgroundColor',
|
||||
'transitionPageMode',
|
||||
'readingMode',
|
||||
'handleBack',
|
||||
'shouldShowTransitionPage',
|
||||
],
|
||||
);
|
||||
501
src/features/reader/components/viewer/ReaderViewer.tsx
Normal file
501
src/features/reader/components/viewer/ReaderViewer.tsx
Normal file
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
* 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 {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useMergedRef } from '@mantine/hooks';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
PageInViewportType,
|
||||
ReaderOpenChapterLocationState,
|
||||
ReaderResumeMode,
|
||||
ReaderStateChapters,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { MediaQuery } from '@/features/core/utils/MediaQuery.tsx';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import {
|
||||
isContinuousReadingMode,
|
||||
isContinuousVerticalReadingMode,
|
||||
shouldApplyReaderWidth,
|
||||
} from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { useMouseDragScroll } from '@/features/core/hooks/useMouseDragScroll.tsx';
|
||||
import { useReaderOverlayContext } from '@/features/reader/contexts/ReaderOverlayContext.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { TReaderOverlayContext } from '@/features/reader/types/ReaderOverlay.types.ts';
|
||||
import { ReaderStatePages } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderAutoScrollContext } from '@/features/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||
import { TReaderTapZoneContext } from '@/features/reader/types/TapZoneLayout.types.ts';
|
||||
import { useReaderTapZoneContext } from '@/features/reader/contexts/ReaderTapZoneContext.tsx';
|
||||
import { useReaderAutoScroll } from '@/features/reader/hooks/useReaderAutoScroll.ts';
|
||||
import { useReaderHideOverlayOnUserScroll } from '@/features/reader/hooks/useReaderHideOverlayOnUserScroll.ts';
|
||||
import { useReaderHorizontalModeInvertXYScrolling } from '@/features/reader/hooks/useReaderHorizontalModeInvertXYScrolling.ts';
|
||||
import { useReaderHideCursorOnInactivity } from '@/features/reader/hooks/useReaderHideCursorOnInactivity.ts';
|
||||
import { useReaderScrollToStartOnPageChange } from '@/features/reader/hooks/useReaderScrollToStartOnPageChange.ts';
|
||||
import { useReaderHandlePageSelection } from '@/features/reader/hooks/useReaderHandlePageSelection.ts';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReaderChapterViewer } from '@/features/reader/components/viewer/ReaderChapterViewer.tsx';
|
||||
import {
|
||||
getPreviousNextChapterVisibility,
|
||||
getReaderChapterViewerCurrentPageIndex,
|
||||
getReaderChapterViewResumeMode,
|
||||
} from '@/features/reader/utils/Reader.utils.ts';
|
||||
import { coerceIn, noOp } from '@/lib/HelperFunctions.ts';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { useReaderPreserveScrollPosition } from '@/features/reader/hooks/useReaderPreserveScrollPosition.ts';
|
||||
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType> = {
|
||||
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
|
||||
[ReadingMode.DOUBLE_PAGE]: PageInViewportType.X,
|
||||
[ReadingMode.CONTINUOUS_VERTICAL]: PageInViewportType.Y,
|
||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: PageInViewportType.X,
|
||||
[ReadingMode.WEBTOON]: PageInViewportType.Y,
|
||||
};
|
||||
|
||||
const BaseReaderViewer = forwardRef(
|
||||
(
|
||||
{
|
||||
currentPageIndex,
|
||||
pageToScrollToIndex,
|
||||
setPageToScrollToIndex,
|
||||
pages,
|
||||
totalPages,
|
||||
setPages,
|
||||
setPageLoadStates,
|
||||
setTotalPages,
|
||||
setCurrentPageIndex,
|
||||
transitionPageMode,
|
||||
retryFailedPagesKeyPrefix,
|
||||
setTransitionPageMode,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
shouldUseInfiniteScroll,
|
||||
readerWidth,
|
||||
pageScaleMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
imagePreLoadAmount,
|
||||
pageGap,
|
||||
customFilter,
|
||||
shouldStretchPage,
|
||||
isStaticNav,
|
||||
readerNavBarWidth,
|
||||
setScrollbarXSize,
|
||||
setScrollbarYSize,
|
||||
isVisible: isOverlayVisible,
|
||||
setIsVisible: setIsOverlayVisible,
|
||||
updateCurrentPageIndex,
|
||||
showPreview,
|
||||
setShowPreview,
|
||||
initialChapter,
|
||||
currentChapter,
|
||||
chapters,
|
||||
visibleChapters,
|
||||
setReaderStateChapters,
|
||||
isCurrentChapterReady,
|
||||
}: Pick<
|
||||
ReaderStatePages,
|
||||
| 'currentPageIndex'
|
||||
| 'pageToScrollToIndex'
|
||||
| 'setPageToScrollToIndex'
|
||||
| 'pages'
|
||||
| 'totalPages'
|
||||
| 'setPages'
|
||||
| 'setPageLoadStates'
|
||||
| 'setTotalPages'
|
||||
| 'setCurrentPageIndex'
|
||||
| 'transitionPageMode'
|
||||
| 'retryFailedPagesKeyPrefix'
|
||||
| 'setTransitionPageMode'
|
||||
> &
|
||||
Pick<
|
||||
IReaderSettings,
|
||||
| 'readingMode'
|
||||
| 'readingDirection'
|
||||
| 'shouldUseInfiniteScroll'
|
||||
| 'readerWidth'
|
||||
| 'pageScaleMode'
|
||||
| 'shouldOffsetDoubleSpreads'
|
||||
| 'imagePreLoadAmount'
|
||||
| 'pageGap'
|
||||
| 'customFilter'
|
||||
| 'shouldStretchPage'
|
||||
| 'isStaticNav'
|
||||
> &
|
||||
Pick<TReaderScrollbarContext, 'setScrollbarXSize' | 'setScrollbarYSize'> &
|
||||
Pick<NavbarContextType, 'readerNavBarWidth'> &
|
||||
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
|
||||
Pick<
|
||||
ReaderStateChapters,
|
||||
| 'initialChapter'
|
||||
| 'currentChapter'
|
||||
| 'chapters'
|
||||
| 'visibleChapters'
|
||||
| 'setReaderStateChapters'
|
||||
| 'isCurrentChapterReady'
|
||||
> &
|
||||
TReaderTapZoneContext & {
|
||||
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
|
||||
},
|
||||
|
||||
ref: ForwardedRef<HTMLDivElement | null>,
|
||||
) => {
|
||||
const { direction: themeDirection } = useTheme();
|
||||
const { resumeMode = ReaderResumeMode.START } = useLocation<ReaderOpenChapterLocationState>().state ?? {
|
||||
resumeMode: ReaderResumeMode.START,
|
||||
};
|
||||
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
const mergedRef = useMergedRef(ref, scrollElementRef);
|
||||
|
||||
const isContinuousVerticalReadingModeActive = isContinuousVerticalReadingMode(readingMode);
|
||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||
const isDragging = useMouseDragScroll(scrollElementRef);
|
||||
|
||||
const automaticScrolling = useReaderAutoScrollContext();
|
||||
useEffect(() => automaticScrolling.setScrollRef(scrollElementRef), []);
|
||||
|
||||
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
|
||||
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
|
||||
useLayoutEffect(() => {
|
||||
setScrollbarXSize(scrollbarXSize);
|
||||
setScrollbarYSize(scrollbarYSize);
|
||||
}, [scrollbarXSize, scrollbarYSize]);
|
||||
|
||||
const handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
|
||||
|
||||
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
|
||||
const [{ minChapterViewWidth, minChapterViewHeight, minChapterSizeSourceChapterId }, setChapterViewerSize] =
|
||||
useState({
|
||||
minChapterViewWidth: 0,
|
||||
minChapterViewHeight: 0,
|
||||
minChapterSizeSourceChapterId: -1,
|
||||
});
|
||||
|
||||
const [, setTriggerReRender] = useState({});
|
||||
|
||||
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode];
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
const initialChapterIndex = useMemo(
|
||||
() => chapters.findIndex((chapter) => chapter.id === initialChapter?.id),
|
||||
[chapters, initialChapter?.id],
|
||||
);
|
||||
const chaptersToRender = useMemo(
|
||||
() =>
|
||||
chapters.slice(
|
||||
Math.max(0, initialChapterIndex - visibleChapters.trailing),
|
||||
Math.min(chapters.length, initialChapterIndex + visibleChapters.leading + 1),
|
||||
),
|
||||
[chapters, initialChapterIndex, visibleChapters.trailing, visibleChapters.leading],
|
||||
);
|
||||
const currentChapterIndex = useMemo(
|
||||
() => chaptersToRender.findIndex((chapter) => chapter.id === currentChapter?.id),
|
||||
[currentChapter, chaptersToRender],
|
||||
);
|
||||
|
||||
const onChapterViewSizeChange = useCallback(
|
||||
(width: number, height: number, chapterId: ChapterIdInfo['id']) => {
|
||||
if (!isContinuousReadingModeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSameChapterId = chapterId === minChapterSizeSourceChapterId;
|
||||
|
||||
if (isContinuousVerticalReadingModeActive) {
|
||||
if (!isSameChapterId && minChapterViewWidth >= width) {
|
||||
return;
|
||||
}
|
||||
|
||||
setChapterViewerSize({
|
||||
minChapterViewWidth: width,
|
||||
minChapterViewHeight: 0,
|
||||
minChapterSizeSourceChapterId: chapterId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSameChapterId || minChapterViewHeight < height) {
|
||||
setChapterViewerSize({
|
||||
minChapterViewWidth: 0,
|
||||
minChapterViewHeight: height,
|
||||
minChapterSizeSourceChapterId: chapterId,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
isContinuousReadingModeActive,
|
||||
isContinuousVerticalReadingModeActive,
|
||||
minChapterViewWidth,
|
||||
minChapterViewHeight,
|
||||
minChapterSizeSourceChapterId,
|
||||
],
|
||||
);
|
||||
|
||||
useReaderHandlePageSelection(
|
||||
pageToScrollToIndex,
|
||||
currentPageIndex,
|
||||
pages,
|
||||
totalPages,
|
||||
setPageToScrollToIndex,
|
||||
updateCurrentPageIndex,
|
||||
isContinuousReadingModeActive,
|
||||
imageRefs,
|
||||
themeDirection,
|
||||
readingDirection,
|
||||
);
|
||||
useReaderScrollToStartOnPageChange(
|
||||
currentPageIndex,
|
||||
isContinuousReadingModeActive,
|
||||
themeDirection,
|
||||
readingDirection,
|
||||
scrollElementRef,
|
||||
);
|
||||
useReaderHideCursorOnInactivity(scrollElementRef);
|
||||
useReaderHorizontalModeInvertXYScrolling(readingMode, readingDirection, scrollElementRef);
|
||||
useReaderHideOverlayOnUserScroll(
|
||||
isOverlayVisible,
|
||||
setIsOverlayVisible,
|
||||
showPreview,
|
||||
setShowPreview,
|
||||
scrollElementRef,
|
||||
);
|
||||
useReaderAutoScroll(isOverlayVisible, automaticScrolling, isStaticNav);
|
||||
useReaderPreserveScrollPosition(
|
||||
scrollElementRef,
|
||||
currentChapter?.id,
|
||||
currentChapterIndex,
|
||||
currentPageIndex,
|
||||
chaptersToRender,
|
||||
visibleChapters,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
setPageToScrollToIndex,
|
||||
pageScaleMode,
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setChapterViewerSize({
|
||||
minChapterViewWidth: 0,
|
||||
minChapterViewHeight: 0,
|
||||
minChapterSizeSourceChapterId: -1,
|
||||
});
|
||||
setTriggerReRender({});
|
||||
}, [readingMode]);
|
||||
|
||||
if (!initialChapter || !currentChapter) {
|
||||
throw new Error('ReaderViewer: illegal state - initialChapter and currentChapter should not be undefined');
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
ref={mergedRef}
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
flexWrap: 'nowrap',
|
||||
...applyStyles(
|
||||
isContinuousVerticalReadingModeActive && shouldApplyReaderWidth(readerWidth, pageScaleMode),
|
||||
{ alignItems: 'center' },
|
||||
),
|
||||
...applyStyles(!isContinuousVerticalReadingModeActive, {
|
||||
...applyStyles(themeDirection === 'ltr', {
|
||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||
}),
|
||||
...applyStyles(themeDirection === 'rtl', {
|
||||
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
|
||||
}),
|
||||
}),
|
||||
}}
|
||||
onClick={(e) => !isDragging && handleClick(e)}
|
||||
onScroll={() =>
|
||||
ReaderControls.updateCurrentPageOnScroll(
|
||||
imageRefs,
|
||||
totalPages - 1,
|
||||
updateCurrentPageIndex,
|
||||
inViewportType,
|
||||
readingDirection,
|
||||
)
|
||||
}
|
||||
>
|
||||
{chaptersToRender.map((_, index) => {
|
||||
// chapters are sorted by latest to oldest, thus, loop over it in reversed order
|
||||
const chapterIndex = Math.max(0, chaptersToRender.length - index - 1);
|
||||
const chapter = chaptersToRender[chapterIndex];
|
||||
|
||||
const previousChapter =
|
||||
chaptersToRender[chapterIndex + 1] ??
|
||||
chapters[initialChapterIndex + visibleChapters.leading + 1];
|
||||
const nextChapter =
|
||||
chaptersToRender[chapterIndex - 1] ??
|
||||
chapters[initialChapterIndex - visibleChapters.trailing - 1];
|
||||
|
||||
const isInitialChapter = chapter.id === initialChapter.id;
|
||||
const isCurrentChapter = chapter.id === currentChapter.id;
|
||||
const isPreviousChapter = chapter.id === chaptersToRender[currentChapterIndex + 1]?.id;
|
||||
const isNextChapter = chapter.id === chaptersToRender[currentChapterIndex - 1]?.id;
|
||||
const isLeadingChapter = initialChapter.sourceOrder > chapter.sourceOrder;
|
||||
const isTrailingChapter = initialChapter.sourceOrder < chapter.sourceOrder;
|
||||
const isLastLeadingChapter = visibleChapters.lastLeadingChapterSourceOrder === chapter.sourceOrder;
|
||||
const isLastTrailingChapter =
|
||||
visibleChapters.lastTrailingChapterSourceOrder === chapter.sourceOrder;
|
||||
const isPreloadMode =
|
||||
(isLastLeadingChapter && visibleChapters.isLeadingChapterPreloadMode) ||
|
||||
(isLastTrailingChapter && visibleChapters.isTrailingChapterPreloadMode);
|
||||
|
||||
const previousNextChapterVisibility = getPreviousNextChapterVisibility(
|
||||
chapterIndex,
|
||||
chaptersToRender,
|
||||
visibleChapters,
|
||||
);
|
||||
|
||||
const isChapterSizeSourceChapter = chapter.id === minChapterSizeSourceChapterId;
|
||||
|
||||
return (
|
||||
<ReaderChapterViewer
|
||||
key={chapter.id}
|
||||
chapterId={chapter.id}
|
||||
previousChapterId={previousChapter?.id}
|
||||
nextChapterId={nextChapter?.id}
|
||||
isPreviousChapterVisible={previousNextChapterVisibility.previous}
|
||||
isNextChapterVisible={previousNextChapterVisibility.next}
|
||||
lastPageRead={coerceIn(chapter.lastPageRead, 0, chapter.pageCount - 1)}
|
||||
currentPageIndex={getReaderChapterViewerCurrentPageIndex(
|
||||
currentPageIndex,
|
||||
chapter,
|
||||
currentChapter,
|
||||
isCurrentChapter,
|
||||
isCurrentChapterReady,
|
||||
isLeadingChapter,
|
||||
isTrailingChapter,
|
||||
visibleChapters,
|
||||
)}
|
||||
isInitialChapter={isInitialChapter}
|
||||
isCurrentChapter={isCurrentChapter}
|
||||
isPreviousChapter={isPreviousChapter}
|
||||
isNextChapter={isNextChapter}
|
||||
isLeadingChapter={isLeadingChapter}
|
||||
isTrailingChapter={isTrailingChapter}
|
||||
isPreloadMode={isPreloadMode}
|
||||
imageRefs={imageRefs}
|
||||
setPages={setPages}
|
||||
setPageLoadStates={setPageLoadStates}
|
||||
setTotalPages={setTotalPages}
|
||||
setCurrentPageIndex={setCurrentPageIndex}
|
||||
setPageToScrollToIndex={setPageToScrollToIndex}
|
||||
transitionPageMode={transitionPageMode}
|
||||
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
|
||||
readingMode={readingMode}
|
||||
readerWidth={readerWidth}
|
||||
pageScaleMode={pageScaleMode}
|
||||
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads}
|
||||
readingDirection={readingDirection}
|
||||
shouldUseInfiniteScroll={shouldUseInfiniteScroll}
|
||||
updateCurrentPageIndex={isCurrentChapter ? updateCurrentPageIndex : noOp}
|
||||
scrollIntoView={isCurrentChapter && visibleChapters.scrollIntoView}
|
||||
resumeMode={getReaderChapterViewResumeMode(
|
||||
isCurrentChapter,
|
||||
isInitialChapter,
|
||||
isLeadingChapter,
|
||||
isTrailingChapter,
|
||||
visibleChapters.resumeMode,
|
||||
resumeMode,
|
||||
)}
|
||||
setReaderStateChapters={setReaderStateChapters}
|
||||
setTransitionPageMode={setTransitionPageMode}
|
||||
pageGap={pageGap}
|
||||
imagePreLoadAmount={imagePreLoadAmount}
|
||||
customFilter={customFilter}
|
||||
shouldStretchPage={shouldStretchPage}
|
||||
scrollbarXSize={scrollbarXSize}
|
||||
scrollbarYSize={scrollbarYSize}
|
||||
readerNavBarWidth={readerNavBarWidth}
|
||||
onSizeChange={onChapterViewSizeChange}
|
||||
minWidth={isChapterSizeSourceChapter ? 0 : minChapterViewWidth}
|
||||
minHeight={isChapterSizeSourceChapter ? 0 : minChapterViewHeight}
|
||||
scrollElement={scrollElementRef.current}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ReaderViewer = withPropsFrom(
|
||||
memo(BaseReaderViewer),
|
||||
[
|
||||
userReaderStatePagesContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
useReaderScrollbarContext,
|
||||
useReaderOverlayContext,
|
||||
() => ({ updateCurrentPageIndex: ReaderControls.useUpdateCurrentPageIndex() }),
|
||||
useReaderTapZoneContext,
|
||||
useReaderStateChaptersContext,
|
||||
useNavBarContext,
|
||||
],
|
||||
[
|
||||
'currentPageIndex',
|
||||
'pageToScrollToIndex',
|
||||
'setPageToScrollToIndex',
|
||||
'pages',
|
||||
'totalPages',
|
||||
'setPages',
|
||||
'setPageLoadStates',
|
||||
'setTotalPages',
|
||||
'setCurrentPageIndex',
|
||||
'retryFailedPagesKeyPrefix',
|
||||
'setTransitionPageMode',
|
||||
'readingMode',
|
||||
'readingDirection',
|
||||
'shouldUseInfiniteScroll',
|
||||
'readerWidth',
|
||||
'pageScaleMode',
|
||||
'shouldOffsetDoubleSpreads',
|
||||
'imagePreLoadAmount',
|
||||
'pageGap',
|
||||
'customFilter',
|
||||
'shouldStretchPage',
|
||||
'isStaticNav',
|
||||
'readerNavBarWidth',
|
||||
'transitionPageMode',
|
||||
'setScrollbarXSize',
|
||||
'setScrollbarYSize',
|
||||
'isVisible',
|
||||
'setIsVisible',
|
||||
'updateCurrentPageIndex',
|
||||
'showPreview',
|
||||
'setShowPreview',
|
||||
'initialChapter',
|
||||
'currentChapter',
|
||||
'chapters',
|
||||
'visibleChapters',
|
||||
'setReaderStateChapters',
|
||||
'isCurrentChapterReady',
|
||||
],
|
||||
);
|
||||
160
src/features/reader/components/viewer/pager/BasePager.tsx
Normal file
160
src/features/reader/components/viewer/pager/BasePager.tsx
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 { forwardRef, memo, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import Box, { BoxProps } from '@mui/material/Box';
|
||||
import { getPageIndexesToLoad, isATransitionPageVisible } from '@/features/reader/utils/ReaderPager.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderPagerProps,
|
||||
ReaderResumeMode,
|
||||
ReaderTransitionPageMode,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { isContinuousReadingMode } from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
|
||||
const getPreviousCurrentPageIndex = (resumeMode: ReaderResumeMode): number =>
|
||||
resumeMode === ReaderResumeMode.END ? Number.MAX_SAFE_INTEGER : -1;
|
||||
|
||||
const BaseBasePager = forwardRef<
|
||||
HTMLDivElement,
|
||||
Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix' | 'isPreloadMode'> &
|
||||
Pick<IReaderSettings, 'readingMode' | 'imagePreLoadAmount'> & {
|
||||
createPage: (
|
||||
page: ReaderStatePages['pages'][number],
|
||||
pagesIndex: number,
|
||||
shouldLoad: boolean,
|
||||
shouldDisplay: boolean,
|
||||
setRef: (pagesIndex: number, element: HTMLElement | null) => void,
|
||||
readingMode: ReaderPagerProps['readingMode'],
|
||||
customFilter: ReaderPagerProps['customFilter'],
|
||||
pageScaleMode: ReaderPagerProps['pageScaleMode'],
|
||||
shouldStretchPage: ReaderPagerProps['shouldStretchPage'],
|
||||
readerWidth: ReaderPagerProps['readerWidth'],
|
||||
scrollbarXSize: ReaderPagerProps['scrollbarXSize'],
|
||||
scrollbarYSize: ReaderPagerProps['scrollbarYSize'],
|
||||
readerNavBarWidth: ReaderPagerProps['readerNavBarWidth'],
|
||||
) => ReactNode;
|
||||
slots?: { boxProps?: BoxProps };
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
currentPageIndex,
|
||||
pages,
|
||||
transitionPageMode,
|
||||
imageRefs,
|
||||
createPage,
|
||||
slots,
|
||||
readingMode,
|
||||
imagePreLoadAmount,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
customFilter,
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
readerWidth,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
readerNavBarWidth,
|
||||
resumeMode,
|
||||
handleAsInitialRender,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const previousCurrentPageIndex = useRef(getPreviousCurrentPageIndex(resumeMode));
|
||||
|
||||
if (handleAsInitialRender) {
|
||||
previousCurrentPageIndex.current = getPreviousCurrentPageIndex(resumeMode);
|
||||
}
|
||||
|
||||
const pagesIndexesToRender = useMemo(
|
||||
() =>
|
||||
getPageIndexesToLoad(
|
||||
currentPageIndex,
|
||||
pages,
|
||||
previousCurrentPageIndex.current,
|
||||
imagePreLoadAmount,
|
||||
readingMode,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
),
|
||||
[
|
||||
currentPageIndex,
|
||||
pages,
|
||||
imagePreLoadAmount,
|
||||
readingMode,
|
||||
isCurrentChapter,
|
||||
isPreviousChapter,
|
||||
isNextChapter,
|
||||
],
|
||||
);
|
||||
useEffect(() => {
|
||||
if (isCurrentChapter) {
|
||||
previousCurrentPageIndex.current = currentPageIndex;
|
||||
}
|
||||
}, [pagesIndexesToRender, isCurrentChapter]);
|
||||
|
||||
const setRef = useCallback(
|
||||
(pagesIndex: number, element: HTMLElement | null) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
imageRefs.current[pagesIndex] = element;
|
||||
},
|
||||
[imageRefs],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
{...slots?.boxProps}
|
||||
sx={[
|
||||
{
|
||||
width: 'fit-content',
|
||||
height: 'fit-content',
|
||||
},
|
||||
...(Array.isArray(slots?.boxProps?.sx) ? (slots?.boxProps?.sx ?? []) : [slots?.boxProps?.sx]),
|
||||
// hide pager, without actually unmounting it to prevent re-renders, while a chapter transition page is taking up the full screen
|
||||
applyStyles(
|
||||
!isContinuousReadingMode(readingMode) &&
|
||||
isATransitionPageVisible(transitionPageMode, readingMode),
|
||||
{
|
||||
visibility: 'hidden',
|
||||
width: 0,
|
||||
height: 0,
|
||||
m: 0,
|
||||
p: 0,
|
||||
},
|
||||
),
|
||||
]}
|
||||
>
|
||||
{pages.map((page, pagesIndex) =>
|
||||
createPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
pagesIndexesToRender.includes(pagesIndex),
|
||||
[ReaderTransitionPageMode.NONE, ReaderTransitionPageMode.BOTH].includes(transitionPageMode),
|
||||
setRef,
|
||||
readingMode,
|
||||
customFilter,
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
readerWidth,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
readerNavBarWidth,
|
||||
),
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const BasePager = memo(BaseBasePager);
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 { Direction, useTheme } from '@mui/material/styles';
|
||||
import { forwardRef, Fragment, memo, useMemo } from 'react';
|
||||
import { BasePager } from '@/features/reader/components/viewer/pager/BasePager.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderPagerProps,
|
||||
ReaderPageScaleMode,
|
||||
ReadingDirection,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { createReaderPage } from '@/features/reader/utils/ReaderPager.utils.tsx';
|
||||
import { getNextIndexFromPage, getPage } from '@/features/reader/utils/ReaderProgressBar.utils.tsx';
|
||||
|
||||
const getPagePosition = (
|
||||
pageType: 'first' | 'second',
|
||||
themeDirection: Direction,
|
||||
readingDirection: ReadingDirection,
|
||||
): 'left' | 'right' => {
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
|
||||
if (pageType === 'first') {
|
||||
if (themeDirection === 'ltr') {
|
||||
return isLtrReadingDirection ? 'right' : 'left';
|
||||
}
|
||||
|
||||
return isLtrReadingDirection ? 'left' : 'right';
|
||||
}
|
||||
|
||||
if (themeDirection === 'ltr') {
|
||||
return isLtrReadingDirection ? 'left' : 'right';
|
||||
}
|
||||
|
||||
return isLtrReadingDirection ? 'right' : 'left';
|
||||
};
|
||||
|
||||
const BaseReaderDoublePagedPager = forwardRef<
|
||||
HTMLDivElement,
|
||||
ReaderPagerProps & Pick<IReaderSettings, 'readingDirection' | 'pageScaleMode'>
|
||||
>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => {
|
||||
const { currentPageIndex, pages, totalPages, readingDirection, pageScaleMode } = props;
|
||||
|
||||
const { direction: themeDirection } = useTheme();
|
||||
|
||||
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
|
||||
return (
|
||||
<BasePager
|
||||
ref={ref}
|
||||
{...props}
|
||||
createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) => {
|
||||
const { primary, secondary } = page;
|
||||
|
||||
const currentSecondaryPageIndex = getNextIndexFromPage(currentPage);
|
||||
|
||||
const hasSecondaryPage = !!secondary;
|
||||
const isPrimaryPage = currentPage.primary.index === primary.index;
|
||||
const isSecondaryPage = !!secondary && currentSecondaryPageIndex === secondary.index;
|
||||
|
||||
return (
|
||||
<Fragment key={`${primary.url}_${secondary?.url}`}>
|
||||
{createReaderPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
true,
|
||||
pageLoadStates[primary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
shouldDisplay && isPrimaryPage && shouldLoad,
|
||||
currentPage.primary.index,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
hasSecondaryPage ? getPagePosition('first', themeDirection, readingDirection) : undefined,
|
||||
hasSecondaryPage,
|
||||
)}
|
||||
{hasSecondaryPage &&
|
||||
createReaderPage(
|
||||
{ ...page, primary: { ...page.secondary! } },
|
||||
pagesIndex,
|
||||
false,
|
||||
pageLoadStates[secondary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
shouldDisplay && isSecondaryPage && shouldLoad,
|
||||
currentSecondaryPageIndex,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[secondary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
getPagePosition('second', themeDirection, readingDirection),
|
||||
true,
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}}
|
||||
slots={{
|
||||
boxProps: {
|
||||
sx: {
|
||||
...applyStyles(pageScaleMode === ReaderPageScaleMode.ORIGINAL, {
|
||||
margin: 'auto',
|
||||
}),
|
||||
width: '100%',
|
||||
minWidth: 'fit-content',
|
||||
height: '100%',
|
||||
minHeight: 'fit-content',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'nowrap',
|
||||
...applyStyles(themeDirection === 'ltr', {
|
||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||
}),
|
||||
...applyStyles(themeDirection === 'rtl', {
|
||||
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const ReaderDoublePagedPager = memo(BaseReaderDoublePagedPager);
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 { useTheme } from '@mui/material/styles';
|
||||
import { forwardRef, memo } from 'react';
|
||||
import { BasePager } from '@/features/reader/components/viewer/pager/BasePager.tsx';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/features/reader/types/Reader.types.ts';
|
||||
import { createReaderPage } from '@/features/reader/utils/ReaderPager.utils.tsx';
|
||||
|
||||
const BaseReaderHorizontalPager = forwardRef<
|
||||
HTMLDivElement,
|
||||
ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingDirection'>
|
||||
>(({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => {
|
||||
const { currentPageIndex, totalPages, pageGap, readingDirection } = props;
|
||||
|
||||
const { direction: themeDirection } = useTheme();
|
||||
|
||||
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
|
||||
|
||||
return (
|
||||
<BasePager
|
||||
ref={ref}
|
||||
{...props}
|
||||
createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) =>
|
||||
createReaderPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
true,
|
||||
pageLoadStates[page.primary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
!isPreloadMode,
|
||||
currentPageIndex,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
setRef,
|
||||
)
|
||||
}
|
||||
slots={{
|
||||
boxProps: {
|
||||
sx: {
|
||||
my: 'auto',
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
alignItems: 'center',
|
||||
gap: `${pageGap}px`,
|
||||
...applyStyles(themeDirection === 'ltr', {
|
||||
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
|
||||
justifyContent: isLtrReadingDirection ? 'flex-start' : 'flex-end',
|
||||
}),
|
||||
...applyStyles(themeDirection === 'rtl', {
|
||||
flexDirection: isLtrReadingDirection ? 'row-reverse' : 'row',
|
||||
justifyContent: isLtrReadingDirection ? 'flex-end' : 'flex-start',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const ReaderHorizontalPager = memo(BaseReaderHorizontalPager);
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { forwardRef, memo } from 'react';
|
||||
import { BasePager } from '@/features/reader/components/viewer/pager/BasePager.tsx';
|
||||
import { ReaderPagerProps } from '@/features/reader/types/Reader.types.ts';
|
||||
import { createReaderPage } from '@/features/reader/utils/ReaderPager.utils.tsx';
|
||||
|
||||
const BaseReaderPagedPager = forwardRef<HTMLDivElement, ReaderPagerProps>(
|
||||
({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => {
|
||||
const { currentPageIndex, totalPages } = props;
|
||||
|
||||
return (
|
||||
<BasePager
|
||||
ref={ref}
|
||||
{...props}
|
||||
createPage={(page, pagesIndex, shouldLoad, shouldDisplay, _setRef, ...baseProps) =>
|
||||
createReaderPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
true,
|
||||
pageLoadStates[page.primary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
shouldDisplay && shouldLoad && currentPageIndex === page.primary.index,
|
||||
currentPageIndex,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
)
|
||||
}
|
||||
slots={{
|
||||
boxProps: {
|
||||
sx: {
|
||||
margin: 'auto',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ReaderPagedPager = memo(BaseReaderPagedPager);
|
||||
@@ -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 { forwardRef, memo } from 'react';
|
||||
import { BasePager } from '@/features/reader/components/viewer/pager/BasePager.tsx';
|
||||
import { ReaderPagerProps, ReadingMode } from '@/features/reader/types/Reader.types.ts';
|
||||
import { createReaderPage } from '@/features/reader/utils/ReaderPager.utils.tsx';
|
||||
|
||||
const BaseReaderVerticalPager = forwardRef<HTMLDivElement, ReaderPagerProps>(
|
||||
({ onLoad, onError, pageLoadStates, retryFailedPagesKeyPrefix, isPreloadMode, ...props }, ref) => {
|
||||
const { currentPageIndex, totalPages, readingMode, pageGap } = props;
|
||||
|
||||
const isWebtoonMode = readingMode === ReadingMode.WEBTOON;
|
||||
const actualPageGap = isWebtoonMode ? 0 : pageGap;
|
||||
|
||||
return (
|
||||
<BasePager
|
||||
ref={ref}
|
||||
{...props}
|
||||
createPage={(page, pagesIndex, shouldLoad, _, setRef, ...baseProps) =>
|
||||
createReaderPage(
|
||||
page,
|
||||
pagesIndex,
|
||||
true,
|
||||
pageLoadStates[page.primary.index].loaded,
|
||||
isPreloadMode,
|
||||
onLoad,
|
||||
onError,
|
||||
shouldLoad,
|
||||
!isPreloadMode,
|
||||
currentPageIndex,
|
||||
totalPages,
|
||||
...baseProps,
|
||||
pageLoadStates[page.primary.index].error ? retryFailedPagesKeyPrefix : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
page.primary.index !== 0 ? actualPageGap : 0,
|
||||
setRef,
|
||||
)
|
||||
}
|
||||
slots={{ boxProps: { sx: { margin: 'auto' } } }}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ReaderVerticalPager = memo(BaseReaderVerticalPager);
|
||||
30
src/features/reader/constants/ReaderContext.constants.ts
Normal file
30
src/features/reader/constants/ReaderContext.constants.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { ReaderStatePages } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
import { ReaderTransitionPageMode } from '@/features/reader/types/Reader.types.ts';
|
||||
import { createPageData } from '@/features/reader/utils/ReaderPager.utils.tsx';
|
||||
|
||||
export const READER_STATE_PAGES_DEFAULTS: ReaderStatePages = {
|
||||
totalPages: 0,
|
||||
setTotalPages: () => undefined,
|
||||
currentPageIndex: 0,
|
||||
setCurrentPageIndex: () => undefined,
|
||||
pageToScrollToIndex: null,
|
||||
setPageToScrollToIndex: () => undefined,
|
||||
pageUrls: [],
|
||||
setPageUrls: () => undefined,
|
||||
pageLoadStates: [{ url: '', loaded: false }],
|
||||
setPageLoadStates: () => undefined,
|
||||
pages: [createPageData('', 0)],
|
||||
setPages: () => undefined,
|
||||
transitionPageMode: ReaderTransitionPageMode.NONE,
|
||||
setTransitionPageMode: () => undefined,
|
||||
retryFailedPagesKeyPrefix: '',
|
||||
setRetryFailedPagesKeyPrefix: () => undefined,
|
||||
};
|
||||
414
src/features/reader/constants/ReaderSettings.constants.tsx
Normal file
414
src/features/reader/constants/ReaderSettings.constants.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
* 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 ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft';
|
||||
import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight';
|
||||
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
|
||||
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 { ScrollDirection, ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsGlobal,
|
||||
ProgressBarPosition,
|
||||
ProgressBarType,
|
||||
ReaderBackgroundColor,
|
||||
ReaderBlendMode,
|
||||
ReaderExitMode,
|
||||
ReaderHotkey,
|
||||
ReaderOverlayMode,
|
||||
ReaderPageScaleMode,
|
||||
ReaderScrollAmount,
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { SinglePageIcon } from '@/assets/icons/svg/SinglePageIcon.tsx';
|
||||
import { DoublePageIcon } from '@/assets/icons/svg/DoublePageIcon.tsx';
|
||||
import { ContinuousVerticalPageIcon } from '@/assets/icons/svg/ContinuousVerticalPageIcon.tsx';
|
||||
import { ContinuousHorizontalPageIcon } from '@/assets/icons/svg/ContinuousHorizontalPageIcon.tsx';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
import { TapZoneLayouts } from '@/features/reader/types/TapZoneLayout.types.ts';
|
||||
import { WebtoonPageIcon } from '@/assets/icons/svg/WebtoonPageIcon.tsx';
|
||||
|
||||
export const AUTO_SCROLL_SPEED = {
|
||||
min: 0.5,
|
||||
max: 60,
|
||||
step: 0.5,
|
||||
default: 5,
|
||||
};
|
||||
|
||||
export const SCROLL_AMOUNT = {
|
||||
min: 5,
|
||||
max: 100,
|
||||
default: ReaderScrollAmount.LARGE,
|
||||
step: 5,
|
||||
};
|
||||
|
||||
export const PROGRESS_BAR_SIZE = {
|
||||
min: 2,
|
||||
max: 20,
|
||||
step: 1,
|
||||
default: 4,
|
||||
};
|
||||
|
||||
export const IMAGE_PRE_LOAD_AMOUNT = {
|
||||
min: 1,
|
||||
max: 20,
|
||||
default: 5,
|
||||
step: 1,
|
||||
};
|
||||
|
||||
export const PAGE_GAP = {
|
||||
min: 0,
|
||||
max: 20,
|
||||
default: 5,
|
||||
step: 1,
|
||||
};
|
||||
|
||||
export const CUSTOM_FILTER = {
|
||||
brightness: {
|
||||
min: 5,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 100,
|
||||
},
|
||||
contrast: {
|
||||
min: 5,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 100,
|
||||
},
|
||||
saturate: {
|
||||
min: 0,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 100,
|
||||
},
|
||||
hue: {
|
||||
min: 0,
|
||||
max: 200,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
rgba: {
|
||||
red: {
|
||||
min: 0,
|
||||
max: 255,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
green: {
|
||||
min: 0,
|
||||
max: 255,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
blue: {
|
||||
min: 0,
|
||||
max: 255,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
alpha: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const READING_DIRECTION_TO_THEME_DIRECTION: Record<ReadingDirection, Direction> = {
|
||||
[ReadingDirection.LTR]: 'ltr',
|
||||
[ReadingDirection.RTL]: 'rtl',
|
||||
};
|
||||
|
||||
const GLOBAL_READER_SETTING_OBJECT: Record<keyof IReaderSettingsGlobal, undefined> = {
|
||||
overlayMode: undefined,
|
||||
exitMode: undefined,
|
||||
customFilter: undefined,
|
||||
shouldSkipDupChapters: undefined,
|
||||
shouldSkipFilteredChapters: undefined,
|
||||
progressBarType: undefined,
|
||||
progressBarSize: undefined,
|
||||
progressBarPosition: undefined,
|
||||
progressBarPositionAutoVertical: undefined,
|
||||
shouldShowPageNumber: undefined,
|
||||
isStaticNav: undefined,
|
||||
backgroundColor: undefined,
|
||||
hotkeys: undefined,
|
||||
imagePreLoadAmount: undefined,
|
||||
shouldUseAutoWebtoonMode: undefined,
|
||||
autoScroll: undefined,
|
||||
shouldShowReadingModePreview: undefined,
|
||||
shouldShowTapZoneLayoutPreview: undefined,
|
||||
shouldInformAboutMissingChapter: undefined,
|
||||
shouldInformAboutScanlatorChange: undefined,
|
||||
scrollAmount: undefined,
|
||||
shouldUseInfiniteScroll: undefined,
|
||||
shouldShowTransitionPage: undefined,
|
||||
};
|
||||
|
||||
export const GLOBAL_READER_SETTING_KEYS = Object.keys(GLOBAL_READER_SETTING_OBJECT);
|
||||
|
||||
export const DEFAULT_READER_SETTINGS: IReaderSettings = {
|
||||
readerWidth: { value: 50, enabled: false },
|
||||
overlayMode: ReaderOverlayMode.AUTO,
|
||||
tapZoneLayout: TapZoneLayouts.RIGHT_LEFT,
|
||||
tapZoneInvertMode: { vertical: false, horizontal: false },
|
||||
progressBarType: ProgressBarType.STANDARD,
|
||||
progressBarSize: PROGRESS_BAR_SIZE.default,
|
||||
progressBarPosition: ProgressBarPosition.AUTO,
|
||||
progressBarPositionAutoVertical: ProgressBarPosition.RIGHT,
|
||||
pageScaleMode: ReaderPageScaleMode.ORIGINAL,
|
||||
shouldStretchPage: false,
|
||||
shouldOffsetDoubleSpreads: false,
|
||||
shouldSkipDupChapters: true,
|
||||
shouldSkipFilteredChapters: false,
|
||||
shouldShowPageNumber: true,
|
||||
isStaticNav: false,
|
||||
readingDirection: ReadingDirection.LTR,
|
||||
readingMode: ReadingMode.SINGLE_PAGE,
|
||||
exitMode: ReaderExitMode.PREVIOUS,
|
||||
backgroundColor: ReaderBackgroundColor.THEME,
|
||||
customFilter: {
|
||||
brightness: {
|
||||
value: CUSTOM_FILTER.brightness.default,
|
||||
enabled: false,
|
||||
},
|
||||
contrast: {
|
||||
value: CUSTOM_FILTER.contrast.default,
|
||||
enabled: false,
|
||||
},
|
||||
saturate: {
|
||||
value: CUSTOM_FILTER.saturate.default,
|
||||
enabled: false,
|
||||
},
|
||||
hue: {
|
||||
value: CUSTOM_FILTER.hue.default,
|
||||
enabled: false,
|
||||
},
|
||||
rgba: {
|
||||
value: {
|
||||
red: CUSTOM_FILTER.rgba.red.default,
|
||||
green: CUSTOM_FILTER.rgba.green.default,
|
||||
blue: CUSTOM_FILTER.rgba.blue.default,
|
||||
alpha: CUSTOM_FILTER.rgba.alpha.default,
|
||||
blendMode: ReaderBlendMode.DEFAULT,
|
||||
},
|
||||
enabled: false,
|
||||
},
|
||||
sepia: false,
|
||||
grayscale: false,
|
||||
invert: false,
|
||||
},
|
||||
pageGap: PAGE_GAP.default,
|
||||
hotkeys: {
|
||||
[ReaderHotkey.PREVIOUS_PAGE]: ['arrowleft', 'a'],
|
||||
[ReaderHotkey.NEXT_PAGE]: ['arrowright', 'd'],
|
||||
[ReaderHotkey.SCROLL_BACKWARD]: ['arrowup', 'w'],
|
||||
[ReaderHotkey.SCROLL_FORWARD]: ['arrowdown', 's'],
|
||||
[ReaderHotkey.PREVIOUS_CHAPTER]: ['comma'],
|
||||
[ReaderHotkey.NEXT_CHAPTER]: ['period'],
|
||||
[ReaderHotkey.TOGGLE_MENU]: ['m'],
|
||||
[ReaderHotkey.CYCLE_SCALE_TYPE]: ['i'],
|
||||
[ReaderHotkey.STRETCH_IMAGE]: ['f'],
|
||||
[ReaderHotkey.OFFSET_SPREAD_PAGES]: ['o'],
|
||||
[ReaderHotkey.CYCLE_READING_MODE]: ['r'],
|
||||
[ReaderHotkey.CYCLE_READING_DIRECTION]: ['t'],
|
||||
[ReaderHotkey.TOGGLE_AUTO_SCROLL]: ['space'],
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE]: ['b'],
|
||||
[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE]: ['v'],
|
||||
[ReaderHotkey.EXIT_READER]: ['c'],
|
||||
},
|
||||
imagePreLoadAmount: IMAGE_PRE_LOAD_AMOUNT.default,
|
||||
shouldUseAutoWebtoonMode: true,
|
||||
autoScroll: {
|
||||
value: AUTO_SCROLL_SPEED.default,
|
||||
smooth: true,
|
||||
},
|
||||
shouldShowReadingModePreview: true,
|
||||
shouldShowTapZoneLayoutPreview: true,
|
||||
shouldInformAboutMissingChapter: true,
|
||||
shouldInformAboutScanlatorChange: true,
|
||||
scrollAmount: ReaderScrollAmount.LARGE,
|
||||
shouldUseInfiniteScroll: true,
|
||||
shouldShowTransitionPage: true,
|
||||
};
|
||||
|
||||
export const READER_PROGRESS_BAR_POSITION_TO_PLACEMENT: Record<ProgressBarPosition, TooltipProps['placement']> = {
|
||||
[ProgressBarPosition.BOTTOM]: 'top',
|
||||
[ProgressBarPosition.LEFT]: 'right',
|
||||
[ProgressBarPosition.RIGHT]: 'left',
|
||||
// should never get accessed
|
||||
[ProgressBarPosition.AUTO]: 'left',
|
||||
};
|
||||
|
||||
export const READING_DIRECTION_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReadingDirection> = {
|
||||
[ReadingDirection.LTR]: {
|
||||
title: 'reader.settings.reading_direction.ltr',
|
||||
icon: <ArrowCircleRightIcon />,
|
||||
},
|
||||
[ReadingDirection.RTL]: {
|
||||
title: 'reader.settings.reading_direction.rtl',
|
||||
icon: <ArrowCircleLeftIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export const READING_DIRECTION_VALUES = Object.values(ReadingDirection).filter((value) => typeof value === 'number');
|
||||
|
||||
export const PAGE_SCALE_VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderPageScaleMode> = {
|
||||
[ReaderPageScaleMode.WIDTH]: {
|
||||
title: 'reader.settings.page_scale.width',
|
||||
icon: <ExpandIcon sx={{ transform: 'rotate(90deg)' }} />,
|
||||
},
|
||||
[ReaderPageScaleMode.HEIGHT]: {
|
||||
title: 'reader.settings.page_scale.height',
|
||||
icon: <ExpandIcon />,
|
||||
},
|
||||
[ReaderPageScaleMode.SCREEN]: {
|
||||
title: 'reader.settings.page_scale.screen',
|
||||
icon: <ZoomOutMapIcon />,
|
||||
},
|
||||
[ReaderPageScaleMode.ORIGINAL]: {
|
||||
title: 'reader.settings.page_scale.original',
|
||||
icon: <CropOriginalIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export const READER_PAGE_SCALE_MODE_VALUES = Object.values(ReaderPageScaleMode).filter(
|
||||
(value) => typeof value === 'number',
|
||||
);
|
||||
|
||||
export const READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED: Record<ReaderPageScaleMode, boolean> = {
|
||||
[ReaderPageScaleMode.WIDTH]: true,
|
||||
[ReaderPageScaleMode.HEIGHT]: true,
|
||||
[ReaderPageScaleMode.SCREEN]: true,
|
||||
[ReaderPageScaleMode.ORIGINAL]: false,
|
||||
};
|
||||
|
||||
export const READING_MODE_VALUE_TO_DISPLAY_DATA = {
|
||||
[ReadingMode.SINGLE_PAGE]: {
|
||||
title: 'reader.settings.reader_type.label.single_page',
|
||||
icon: <SinglePageIcon />,
|
||||
},
|
||||
[ReadingMode.DOUBLE_PAGE]: {
|
||||
title: 'reader.settings.reader_type.label.double_page',
|
||||
icon: <DoublePageIcon />,
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_VERTICAL]: {
|
||||
title: 'reader.settings.reader_type.label.continuous_vertical',
|
||||
icon: <ContinuousVerticalPageIcon />,
|
||||
},
|
||||
[ReadingMode.CONTINUOUS_HORIZONTAL]: {
|
||||
title: 'reader.settings.reader_type.label.continuous_horizontal',
|
||||
icon: <ContinuousHorizontalPageIcon />,
|
||||
},
|
||||
[ReadingMode.WEBTOON]: {
|
||||
title: 'reader.settings.reader_type.label.webtoon',
|
||||
icon: <WebtoonPageIcon />,
|
||||
},
|
||||
} satisfies ValueToDisplayData<ReadingMode>;
|
||||
|
||||
export const READING_MODE_VALUES = Object.values(ReadingMode).filter((value) => typeof value === 'number');
|
||||
|
||||
export enum ReaderSettingTab {
|
||||
LAYOUT,
|
||||
GENERAL,
|
||||
FILTER,
|
||||
BEHAVIOUR,
|
||||
HOTKEYS,
|
||||
}
|
||||
|
||||
export const READER_SETTING_TABS: Record<
|
||||
ReaderSettingTab,
|
||||
{
|
||||
id: ReaderSettingTab;
|
||||
label: TranslationKey;
|
||||
supportsTouchDevices: boolean;
|
||||
}
|
||||
> = {
|
||||
[ReaderSettingTab.LAYOUT]: {
|
||||
id: ReaderSettingTab.LAYOUT,
|
||||
label: 'reader.settings.label.layout',
|
||||
supportsTouchDevices: true,
|
||||
},
|
||||
[ReaderSettingTab.GENERAL]: {
|
||||
id: ReaderSettingTab.GENERAL,
|
||||
label: 'global.label.general',
|
||||
supportsTouchDevices: true,
|
||||
},
|
||||
[ReaderSettingTab.FILTER]: {
|
||||
id: ReaderSettingTab.FILTER,
|
||||
label: 'reader.settings.custom_filter.title',
|
||||
supportsTouchDevices: true,
|
||||
},
|
||||
[ReaderSettingTab.BEHAVIOUR]: {
|
||||
id: ReaderSettingTab.BEHAVIOUR,
|
||||
label: 'reader.settings.label.behaviour',
|
||||
supportsTouchDevices: true,
|
||||
},
|
||||
[ReaderSettingTab.HOTKEYS]: {
|
||||
id: ReaderSettingTab.HOTKEYS,
|
||||
label: 'hotkeys.title_other',
|
||||
supportsTouchDevices: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const READER_HOTKEYS = Object.values(ReaderHotkey).filter(
|
||||
(hotkey) => typeof hotkey === 'number',
|
||||
) as ReaderHotkey[];
|
||||
|
||||
export const READER_BACKGROUND_TO_COLOR = {
|
||||
[ReaderBackgroundColor.THEME]: 'background.default',
|
||||
[ReaderBackgroundColor.BLACK]: 'common.black',
|
||||
[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 READER_BLEND_MODE_VALUE_TO_DISPLAY_DATA = {
|
||||
[ReaderBlendMode.DEFAULT]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.default',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.MULTIPLY]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.multiply',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.SCREEN]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.screen',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.OVERLAY]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.overlay',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.DARKEN]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.darken',
|
||||
icon: null,
|
||||
},
|
||||
[ReaderBlendMode.LIGHTEN]: {
|
||||
title: 'reader.settings.custom_filter.rgba.blend_mode.lighten',
|
||||
icon: null,
|
||||
},
|
||||
} satisfies ValueToDisplayData<ReaderBlendMode>;
|
||||
|
||||
export const READER_BLEND_MODE_VALUES = Object.values(ReaderBlendMode);
|
||||
151
src/features/reader/constants/ReaderTapZone.constants.ts
Normal file
151
src/features/reader/constants/ReaderTapZone.constants.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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 { TapZoneLayouts, TapZoneRegion, TapZoneRegionType } from '@/features/reader/types/TapZoneLayout.types.ts';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
export const READER_TAP_ZONE_LAYOUTS: Record<TapZoneLayouts, TapZoneRegion[]> = {
|
||||
/**
|
||||
* +---+---+---+
|
||||
* | N | N | N | P: Previous
|
||||
* +---+---+---+
|
||||
* | N | M | N | M: Menu
|
||||
* +---+---+---+
|
||||
* | N | P | N | N: Next
|
||||
* +---+---+---+
|
||||
*/
|
||||
[TapZoneLayouts.EDGE]: [
|
||||
{
|
||||
rect: [0, 0, 100, 33.33],
|
||||
type: TapZoneRegionType.NEXT,
|
||||
},
|
||||
{
|
||||
rect: [0, 33.33, 33.33, 66.66],
|
||||
type: TapZoneRegionType.NEXT,
|
||||
},
|
||||
{
|
||||
rect: [66.66, 33.33, 33.33, 66.66],
|
||||
type: TapZoneRegionType.NEXT,
|
||||
},
|
||||
{
|
||||
rect: [33.33, 33.33, 33.33, 33.33],
|
||||
type: TapZoneRegionType.MENU,
|
||||
},
|
||||
{
|
||||
rect: [33.33, 66.66, 33.33, 33.33],
|
||||
type: TapZoneRegionType.PREVIOUS,
|
||||
},
|
||||
],
|
||||
/**
|
||||
* +---+---+---+
|
||||
* | M | M | M | P: Previous
|
||||
* +---+---+---+
|
||||
* | P | N | N | M: Menu
|
||||
* +---+---+---+
|
||||
* | P | N | N | N: Next
|
||||
* +---+---+---+
|
||||
*/
|
||||
[TapZoneLayouts.KINDLE]: [
|
||||
{
|
||||
rect: [0, 0, 100, 33.33],
|
||||
type: TapZoneRegionType.MENU,
|
||||
},
|
||||
{
|
||||
rect: [0, 33.33, 33.33, 66.66],
|
||||
type: TapZoneRegionType.PREVIOUS,
|
||||
},
|
||||
{
|
||||
rect: [33.33, 33.33, 66.66, 66.66],
|
||||
type: TapZoneRegionType.NEXT,
|
||||
},
|
||||
],
|
||||
/**
|
||||
* +---+---+---+
|
||||
* | P | P | P | P: Previous
|
||||
* +---+---+---+
|
||||
* | P | M | N | M: Menu
|
||||
* +---+---+---+
|
||||
* | N | N | N | N: Next
|
||||
* +---+---+---+
|
||||
*/
|
||||
[TapZoneLayouts.L_SHAPE]: [
|
||||
{
|
||||
rect: [0, 0, 100, 33.33],
|
||||
type: TapZoneRegionType.PREVIOUS,
|
||||
},
|
||||
{
|
||||
rect: [0, 33.33, 33.33, 33.33],
|
||||
type: TapZoneRegionType.PREVIOUS,
|
||||
},
|
||||
{
|
||||
rect: [33.33, 33.33, 33.33, 33.33],
|
||||
type: TapZoneRegionType.MENU,
|
||||
},
|
||||
{
|
||||
rect: [66.66, 33.33, 33.33, 33.33],
|
||||
type: TapZoneRegionType.NEXT,
|
||||
},
|
||||
{
|
||||
rect: [0, 66.66, 100, 33.33],
|
||||
type: TapZoneRegionType.NEXT,
|
||||
},
|
||||
],
|
||||
/**
|
||||
* +---+---+---+
|
||||
* | P | M | N | P: Previous
|
||||
* +---+---+---+
|
||||
* | P | M | N | M: Menu
|
||||
* +---+---+---+
|
||||
* | P | M | N | N: Next
|
||||
* +---+---+---+
|
||||
*/
|
||||
[TapZoneLayouts.RIGHT_LEFT]: [
|
||||
{
|
||||
rect: [0, 0, 33.33, 100],
|
||||
type: TapZoneRegionType.PREVIOUS,
|
||||
},
|
||||
{
|
||||
rect: [33.33, 0, 33.33, 100],
|
||||
type: TapZoneRegionType.MENU,
|
||||
},
|
||||
{
|
||||
rect: [66.66, 0, 33.33, 100],
|
||||
type: TapZoneRegionType.NEXT,
|
||||
},
|
||||
],
|
||||
/**
|
||||
* +---+---+---+
|
||||
* | M | M | M | P: Previous
|
||||
* +---+---+---+
|
||||
* | M | M | M | M: Menu
|
||||
* +---+---+---+
|
||||
* | M | M | M | N: Next
|
||||
* +---+---+---+
|
||||
*/
|
||||
[TapZoneLayouts.DISABLED]: [
|
||||
{
|
||||
rect: [0, 0, 100, 100],
|
||||
type: TapZoneRegionType.MENU,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const TAP_ZONE_REGION_TYPE_DATA: Record<TapZoneRegionType, { text: TranslationKey; color: string }> = {
|
||||
[TapZoneRegionType.PREVIOUS]: {
|
||||
text: 'global.label.previous',
|
||||
color: 'rgba(255, 114, 118, .5)',
|
||||
},
|
||||
[TapZoneRegionType.NEXT]: {
|
||||
text: 'global.label.next',
|
||||
color: 'rgba(144, 238, 144, .5)',
|
||||
},
|
||||
[TapZoneRegionType.MENU]: {
|
||||
text: 'global.label.menu',
|
||||
color: 'rgba(0, 0, 0, .5)',
|
||||
},
|
||||
};
|
||||
24
src/features/reader/contexts/ReaderAutoScrollContext.tsx
Normal file
24
src/features/reader/contexts/ReaderAutoScrollContext.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 '@/features/reader/types/Reader.types.ts';
|
||||
|
||||
export const ReaderAutoScrollContext = createContext<TReaderAutoScrollContext>({
|
||||
isActive: false,
|
||||
isPaused: false,
|
||||
setScrollRef: () => {},
|
||||
start: () => {},
|
||||
cancel: () => {},
|
||||
toggleActive: () => {},
|
||||
pause: () => {},
|
||||
resume: () => {},
|
||||
setDirection: () => {},
|
||||
});
|
||||
|
||||
export const useReaderAutoScrollContext = () => useContext(ReaderAutoScrollContext);
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 '@/features/core/hooks/useAutomaticScrolling.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ReaderScrollAmount,
|
||||
ReadingMode,
|
||||
TReaderAutoScrollContext,
|
||||
} from '@/features/reader/types/Reader.types.ts';
|
||||
import { ReaderAutoScrollContext } from '@/features/reader/contexts/ReaderAutoScrollContext.tsx';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls';
|
||||
import { isContinuousReadingMode } from '@/features/reader/utils/ReaderSettings.utils.tsx';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { CONTINUOUS_READING_MODE_TO_SCROLL_DIRECTION } from '@/features/reader/constants/ReaderSettings.constants.tsx';
|
||||
import { ScrollOffset } from '@/features/core/Core.types.ts';
|
||||
import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
|
||||
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 [direction, setDirection] = useState<ScrollOffset>(ScrollOffset.FORWARD);
|
||||
|
||||
const isScrollingInvertedBasedOnReadingDirection =
|
||||
readingMode === ReadingMode.CONTINUOUS_HORIZONTAL && themeDirection !== combinedDirection;
|
||||
const invertScrolling = isScrollingInvertedBasedOnReadingDirection
|
||||
? getOptionForDirection(
|
||||
direction === ScrollOffset.BACKWARD,
|
||||
direction === ScrollOffset.FORWARD,
|
||||
combinedDirection,
|
||||
)
|
||||
: direction === ScrollOffset.BACKWARD;
|
||||
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
|
||||
|
||||
const changePage = useCallback(() => {
|
||||
openPage('next', 'ltr');
|
||||
}, [openPage]);
|
||||
|
||||
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,
|
||||
setDirection,
|
||||
...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'],
|
||||
);
|
||||
29
src/features/reader/contexts/ReaderContextProvider.tsx
Normal file
29
src/features/reader/contexts/ReaderContextProvider.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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 { ReactNode } from 'react';
|
||||
import { ReaderTapZoneContextProvider } from '@/features/reader/contexts/ReaderTapZoneContextProvider.tsx';
|
||||
import { ReaderProgressBarContextProvider } from '@/features/reader/contexts/ReaderProgressBarContextProvider.tsx';
|
||||
import { ReaderOverlayContextProvider } from '@/features/reader/contexts/ReaderOverlayContextProvider.tsx';
|
||||
import { ReaderStateContextProvider } from '@/features/reader/contexts/state/ReaderStateContextProvider.tsx';
|
||||
import { ReaderScrollbarContextProvider } from '@/features/reader/contexts/ReaderScrollbarContextProvider.tsx';
|
||||
import { ReaderAutoScrollContextProvider } from '@/features/reader/contexts/ReaderAutoScrollContextProvider.tsx';
|
||||
|
||||
export const ReaderContextProvider = ({ children }: { children?: ReactNode }) => (
|
||||
<ReaderStateContextProvider>
|
||||
<ReaderTapZoneContextProvider>
|
||||
<ReaderOverlayContextProvider>
|
||||
<ReaderProgressBarContextProvider>
|
||||
<ReaderScrollbarContextProvider>
|
||||
<ReaderAutoScrollContextProvider>{children}</ReaderAutoScrollContextProvider>
|
||||
</ReaderScrollbarContextProvider>
|
||||
</ReaderProgressBarContextProvider>
|
||||
</ReaderOverlayContextProvider>
|
||||
</ReaderTapZoneContextProvider>
|
||||
</ReaderStateContextProvider>
|
||||
);
|
||||
17
src/features/reader/contexts/ReaderOverlayContext.tsx
Normal file
17
src/features/reader/contexts/ReaderOverlayContext.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { TReaderOverlayContext } from '@/features/reader/types/ReaderOverlay.types.ts';
|
||||
|
||||
export const ReaderOverlayContext = createContext<TReaderOverlayContext>({
|
||||
isVisible: false,
|
||||
setIsVisible: () => {},
|
||||
});
|
||||
|
||||
export const useReaderOverlayContext = () => useContext(ReaderOverlayContext);
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { ReactNode, useMemo, useState } from 'react';
|
||||
import { ReaderOverlayContext } from '@/features/reader/contexts/ReaderOverlayContext.tsx';
|
||||
|
||||
export const ReaderOverlayContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const value = useMemo(() => ({ isVisible, setIsVisible }), [isVisible]);
|
||||
|
||||
return <ReaderOverlayContext.Provider value={value}>{children}</ReaderOverlayContext.Provider>;
|
||||
};
|
||||
19
src/features/reader/contexts/ReaderProgressBarContext.tsx
Normal file
19
src/features/reader/contexts/ReaderProgressBarContext.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 { TReaderProgressBarContext } from '@/features/reader/types/ReaderProgressBar.types.ts';
|
||||
|
||||
export const ReaderProgressBarContext = createContext<TReaderProgressBarContext>({
|
||||
isMaximized: false,
|
||||
setIsMaximized: () => undefined,
|
||||
isDragging: false,
|
||||
setIsDragging: () => undefined,
|
||||
});
|
||||
|
||||
export const useReaderProgressBarContext = () => useContext(ReaderProgressBarContext);
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { ReactNode, useMemo, useState } from 'react';
|
||||
import { ReaderProgressBarContext } from '@/features/reader/contexts/ReaderProgressBarContext.tsx';
|
||||
|
||||
export const ReaderProgressBarContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ isMaximized, setIsMaximized, isDragging, setIsDragging }),
|
||||
[isMaximized, isDragging],
|
||||
);
|
||||
|
||||
return <ReaderProgressBarContext.Provider value={value}>{children}</ReaderProgressBarContext.Provider>;
|
||||
};
|
||||
19
src/features/reader/contexts/ReaderScrollbarContext.tsx
Normal file
19
src/features/reader/contexts/ReaderScrollbarContext.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 { TReaderScrollbarContext } from '@/features/reader/types/Reader.types.ts';
|
||||
|
||||
export const ReaderScrollbarContext = createContext<TReaderScrollbarContext>({
|
||||
scrollbarXSize: 0,
|
||||
setScrollbarXSize: () => undefined,
|
||||
scrollbarYSize: 0,
|
||||
setScrollbarYSize: () => undefined,
|
||||
});
|
||||
|
||||
export const useReaderScrollbarContext = () => useContext(ReaderScrollbarContext);
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { ReactNode, useMemo, useState } from 'react';
|
||||
import { ReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
|
||||
export const ReaderScrollbarContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [scrollbarXSize, setScrollbarXSize] = useState(0);
|
||||
const [scrollbarYSize, setScrollbarYSize] = useState(0);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ scrollbarXSize, setScrollbarXSize, scrollbarYSize, setScrollbarYSize }),
|
||||
[scrollbarXSize, scrollbarYSize],
|
||||
);
|
||||
|
||||
return <ReaderScrollbarContext.Provider value={value}>{children}</ReaderScrollbarContext.Provider>;
|
||||
};
|
||||
17
src/features/reader/contexts/ReaderTapZoneContext.tsx
Normal file
17
src/features/reader/contexts/ReaderTapZoneContext.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { TReaderTapZoneContext } from '@/features/reader/types/TapZoneLayout.types.ts';
|
||||
|
||||
export const ReaderTapZoneContext = createContext<TReaderTapZoneContext>({
|
||||
showPreview: false,
|
||||
setShowPreview: () => undefined,
|
||||
});
|
||||
|
||||
export const useReaderTapZoneContext = () => useContext(ReaderTapZoneContext);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user