Use HOC for reader context usage

This commit is contained in:
schroda
2024-12-21 03:46:55 +01:00
parent 34f484a946
commit f5642af219
27 changed files with 997 additions and 459 deletions

4
src/UtilTypes.d.ts vendored
View File

@@ -33,3 +33,7 @@ type TupleUnion<U extends string | number | symbol, R extends any[] = []> = {
type TransformRecordToWithDefaultFlag<T extends Record<string, any>> = {
[K in keyof T]: { value: T[K]; isDefault: boolean };
};
type MergeObjectsArray<T extends object[]> = T extends [infer F, ...infer R]
? F & MergeObjects<R extends object[] ? R : []>
: {};

View File

@@ -0,0 +1,32 @@
/*
* 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 { ComponentType, forwardRef } from 'react';
type PropsSourceCreator<T> = () => T;
export const withPropsFrom = <
ComponentProps extends Record<string, any>,
SourceProps extends Record<string, any>[],
SourcePropKeys extends keyof (ComponentProps | MergeObjectsArray<SourceProps>),
>(
Component: ComponentType<ComponentProps>,
propsSources: { [K in keyof SourceProps]: PropsSourceCreator<SourceProps[K]> },
sourcePropKeys: SourcePropKeys[],
) =>
forwardRef<HTMLElement, Omit<ComponentProps, SourcePropKeys>>((props, ref) => {
const sourceProps = propsSources.reduce((acc, propsSource) => ({ ...acc, ...propsSource() }), {});
const selectedProps = Object.fromEntries(
Object.entries(sourceProps).filter(([key]) => sourcePropKeys.includes(key as SourcePropKeys)),
) as Pick<MergeObjectsArray<SourceProps>, SourcePropKeys>;
const combinedProps = { ...props, ...selectedProps } as unknown as ComponentProps;
return <Component {...combinedProps} ref={ref} />;
});

View File

@@ -11,29 +11,46 @@ import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ProgressBarType, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import {
IReaderSettings,
ProgressBarType,
ReadingDirection,
TReaderScrollbarContext,
} from '@/modules/reader/types/Reader.types.ts';
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
import { reverseString } from '@/util/Strings.ts';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
import { ReaderStatePages, TReaderProgressBarContext } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderPageNumber = () => {
const { isDesktop } = ReaderService.useOverlayMode();
const { scrollbarXSize } = useReaderScrollbarContext();
const { readerNavBarWidth } = useNavBarContext();
const { isMaximized } = useReaderProgressBarContext();
const { currentPageIndex, pages, totalPages } = userReaderStatePagesContext();
const { progressBarType, shouldShowPageNumber, readingDirection } = ReaderService.useSettings();
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.value === ReadingDirection.LTR ? tmpPageName : reverseString(tmpPageName, SEPARATOR);
}, [currentPageIndex, pages, totalPages, readingDirection.value]);
return readingDirection === ReadingDirection.LTR ? tmpPageName : reverseString(tmpPageName, SEPARATOR);
}, [currentPageIndex, pages, totalPages, readingDirection]);
if (!shouldShowPageNumber) {
return null;
@@ -74,3 +91,27 @@ export const ReaderPageNumber = () => {
</Stack>
);
};
export const ReaderPageNumber = withPropsFrom(
BaseReaderPageNumber,
[
ReaderService.useOverlayMode,
useReaderScrollbarContext,
useNavBarContext,
useReaderProgressBarContext,
userReaderStatePagesContext,
ReaderService.useSettingsWithoutDefaultFlag,
],
[
'isDesktop',
'scrollbarXSize',
'readerNavBarWidth',
'isMaximized',
'currentPageIndex',
'pages',
'totalPages',
'progressBarType',
'shouldShowPageNumber',
'readingDirection',
],
);

View File

@@ -9,18 +9,19 @@
import Box from '@mui/material/Box';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
export const ReaderRGBAFilter = () => {
const { readerNavBarWidth } = useNavBarContext();
const {
customFilter: {
rgba: {
value: { red, green, blue, alpha },
enabled,
},
const BaseReaderRGBAFilter = ({
readerNavBarWidth,
customFilter: {
rgba: {
value: { red, green, blue, alpha },
enabled,
},
} = ReaderService.useSettings();
},
}: Pick<NavbarContextType, 'readerNavBarWidth'> & Pick<IReaderSettings, 'customFilter'>) => {
if (!enabled) {
return null;
}
@@ -39,3 +40,9 @@ export const ReaderRGBAFilter = () => {
/>
);
};
export const ReaderRGBAFilter = withPropsFrom(
BaseReaderRGBAFilter,
[useNavBarContext, ReaderService.useSettingsWithoutDefaultFlag],
['readerNavBarWidth', 'customFilter'],
);

View File

@@ -14,15 +14,23 @@ import { ReaderTapZoneService } from '@/modules/reader/services/ReaderTapZoneSer
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts';
const CANVAS_ID = 'reader-tap-zone-layout-canvas';
export const TapZoneLayout = () => {
const BaseTapZoneLayout = ({
readerNavBarWidth,
showPreview,
tapZoneLayout,
tapZoneInvertMode,
readingDirection,
}: Pick<NavbarContextType, 'readerNavBarWidth'> &
Pick<TReaderTapZoneContext, 'showPreview'> &
Pick<IReaderSettings, 'tapZoneLayout' | 'tapZoneInvertMode' | 'readingDirection'>) => {
const theme = useTheme();
const { readerNavBarWidth } = useNavBarContext();
const { showPreview } = useReaderTapZoneContext();
const { tapZoneLayout, readingDirection, tapZoneInvertMode } = ReaderService.useSettings();
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
@@ -35,10 +43,10 @@ export const TapZoneLayout = () => {
}, [tapZoneLayoutElement]),
);
const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout.value, width, height, theme.typography.h3, {
vertical: tapZoneInvertMode.value.vertical,
horizontal: tapZoneInvertMode.value.horizontal,
isRTL: readingDirection.value === ReadingDirection.RTL,
const canvas = ReaderTapZoneService.getOrCreateCanvas(tapZoneLayout, width, height, theme.typography.h3, {
vertical: tapZoneInvertMode.vertical,
horizontal: tapZoneInvertMode.horizontal,
isRTL: readingDirection === ReadingDirection.RTL,
});
useLayoutEffect(() => {
@@ -62,3 +70,9 @@ export const TapZoneLayout = () => {
</Box>
);
};
export const TapZoneLayout = withPropsFrom(
BaseTapZoneLayout,
[useNavBarContext, useReaderTapZoneContext, ReaderService.useSettingsWithoutDefaultFlag],
['readerNavBarWidth', 'showPreview', 'tapZoneLayout', 'tapZoneInvertMode', 'readingDirection'],
);

View File

@@ -16,10 +16,15 @@ import { ReaderNavBarDesktop } from '@/modules/reader/components/overlay/navigat
import { ReaderOverlayHeaderMobile } from '@/modules/reader/components/overlay/ReaderOverlayHeaderMobile.tsx';
import { ReaderBottomBarMobile } from '@/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderOverlay = ({ isVisible }: BaseReaderOverlayProps & MobileHeaderProps) => {
const { isDesktop, isMobile } = ReaderService.useOverlayMode();
const BaseReaderOverlay = ({
isVisible,
isDesktop,
isMobile,
}: BaseReaderOverlayProps &
MobileHeaderProps &
Pick<ReturnType<typeof ReaderService.useOverlayMode>, 'isDesktop' | 'isMobile'>) => {
const [areSettingsOpen, setAreSettingsOpen] = useState(false);
return (
@@ -44,3 +49,9 @@ export const ReaderOverlay = ({ isVisible }: BaseReaderOverlayProps & MobileHead
</Box>
);
};
export const ReaderOverlay = withPropsFrom(
BaseReaderOverlay,
[ReaderService.useOverlayMode],
['isDesktop', 'isMobile'],
);

View File

@@ -28,21 +28,32 @@ import { actionToTranslationKey, ChapterAction, Chapters } from '@/modules/chapt
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
import { makeToast } from '@/modules/core/utils/Toast.ts';
import { MobileHeaderProps } from '@/modules/reader/types/ReaderOverlay.types.ts';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder';
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
import {
ReaderStateChapters,
TReaderScrollbarContext,
TReaderStateMangaContext,
} from '@/modules/reader/types/Reader.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
const DEFAULT_MANGA = { id: -1, title: '' };
const DEFAULT_CHAPTER = { id: -1, name: '', realUrl: '', isBookmarked: false };
export const ReaderOverlayHeaderMobile = ({ isVisible }: MobileHeaderProps) => {
const BaseReaderOverlayHeaderMobile = ({
isVisible,
manga,
currentChapter,
scrollbarYSize,
}: MobileHeaderProps &
Pick<TReaderStateMangaContext, 'manga'> &
Pick<ReaderStateChapters, 'currentChapter'> &
Pick<TReaderScrollbarContext, 'scrollbarYSize'>) => {
const { t } = useTranslation();
const { manga } = useReaderStateMangaContext();
const { currentChapter } = useReaderStateChaptersContext();
const getOptionForDirection = useGetOptionForDirection();
const { scrollbarYSize } = useReaderScrollbarContext();
const handleBack = useBackButton();
const popupState = usePopupState({ popupId: 'reader-overlay-more-menu', variant: 'popover' });
@@ -128,3 +139,9 @@ export const ReaderOverlayHeaderMobile = ({ isVisible }: MobileHeaderProps) => {
</Slide>
);
};
export const ReaderOverlayHeaderMobile = withPropsFrom(
BaseReaderOverlayHeaderMobile,
[useReaderStateMangaContext, useReaderStateChaptersContext, useReaderScrollbarContext],
['manga', 'currentChapter', 'scrollbarYSize'],
);

View File

@@ -32,6 +32,10 @@ import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/R
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
import { ReaderStateChapters, TReaderStateMangaContext } from '@/modules/reader/types/Reader.types.ts';
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
const useGetPreviousNavBarStaticValue = (isVisible: boolean, isStaticNav: boolean) => {
const wasNavBarStaticRef = useRef(isStaticNav);
@@ -52,17 +56,35 @@ const useGetPreviousNavBarStaticValue = (isVisible: boolean, isStaticNav: boolea
};
const DEFAULT_MANGA: MangaIdInfo = { id: -1 };
export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDesktopProps) => {
const BaseReaderNavBarDesktop = ({
isVisible,
openSettings,
setReaderNavBarWidth,
manga,
chapters,
currentChapter,
previousChapter,
nextChapter,
pages,
currentPageIndex,
pageLoadStates,
setPageLoadStates,
setRetryFailedPagesKeyPrefix,
exit,
}: ReaderNavBarDesktopProps &
Pick<NavbarContextType, 'setReaderNavBarWidth'> &
Pick<TReaderStateMangaContext, 'manga'> &
Pick<ReaderStateChapters, 'currentChapter' | 'previousChapter' | 'nextChapter' | 'chapters'> &
Pick<
ReaderStatePages,
'pages' | 'currentPageIndex' | 'pageLoadStates' | 'setPageLoadStates' | 'setRetryFailedPagesKeyPrefix'
> & {
exit: ReturnType<typeof ReaderService.useExit>;
}) => {
const { t } = useTranslation();
const { setReaderNavBarWidth } = useNavBarContext();
const { manga } = useReaderStateMangaContext();
const { chapters, currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext();
const { pages, currentPageIndex, pageLoadStates, setPageLoadStates, setRetryFailedPagesKeyPrefix } =
userReaderStatePagesContext();
const getOptionForDirection = useGetOptionForDirection();
const exit = ReaderService.useExit();
const updateReaderSettings = ReaderService.useCreateUpdateSetting(manga ?? DEFAULT_MANGA);
const settings = ReaderService.useSettings();
@@ -154,3 +176,28 @@ export const ReaderNavBarDesktop = ({ isVisible, openSettings }: ReaderNavBarDes
</Drawer>
);
};
export const ReaderNavBarDesktop = withPropsFrom(
BaseReaderNavBarDesktop,
[
useNavBarContext,
useReaderStateMangaContext,
useReaderStateChaptersContext,
userReaderStatePagesContext,
() => ({ exit: ReaderService.useExit() }),
],
[
'setReaderNavBarWidth',
'manga',
'chapters',
'currentChapter',
'previousChapter',
'nextChapter',
'pages',
'currentPageIndex',
'pageLoadStates',
'setPageLoadStates',
'setRetryFailedPagesKeyPrefix',
'exit',
],
);

View File

@@ -23,16 +23,20 @@ import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts'
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderNavBarDesktopChapterNavigation = ({
const BaseReaderNavBarDesktopChapterNavigation = ({
currentChapter,
previousChapter,
nextChapter,
chapters = [],
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter' | 'previousChapter' | 'nextChapter'>) => {
readerThemeDirection,
openChapter,
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter' | 'previousChapter' | 'nextChapter'> & {
readerThemeDirection: ReturnType<typeof ReaderService.useGetThemeDirection>;
openChapter: ReturnType<typeof ReaderControls.useOpenChapter>;
}) => {
const { t } = useTranslation();
const readerThemeDirection = ReaderService.useGetThemeDirection();
const openChapter = ReaderControls.useOpenChapter();
const popupState = usePopupState({ variant: 'popover', popupId: 'reader-nav-bar-desktop-chapter-list' });
@@ -111,3 +115,16 @@ export const ReaderNavBarDesktopChapterNavigation = ({
</Stack>
);
};
export const ReaderNavBarDesktopChapterNavigation = withPropsFrom(
BaseReaderNavBarDesktopChapterNavigation,
[
() => ({
readerThemeDirection: ReaderService.useGetThemeDirection(),
}),
() => ({
openChapter: ReaderControls.useOpenChapter(),
}),
],
['readerThemeDirection', 'openChapter'],
);

View File

@@ -20,18 +20,23 @@ import { useGetOptionForDirection } from '@/modules/theme/services/ThemeCreator.
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ReaderNavBarDesktopNextPreviousButton } from '@/modules/reader/components/overlay/navigation/desktop/ReaderNavBarDesktopNextPreviousButton.tsx';
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { IReaderSettings } from '@/modules/reader/types/Reader.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderNavBarDesktopPageNavigation = ({
const BaseReaderNavBarDesktopPageNavigation = ({
currentPageIndex,
pages,
}: Pick<ReaderStatePages, 'currentPageIndex' | 'pages'>) => {
readingDirection,
openPage,
}: Pick<ReaderStatePages, 'currentPageIndex' | 'pages'> &
Pick<IReaderSettings, 'readingDirection'> & {
openPage: ReturnType<typeof ReaderControls.useOpenPage>;
}) => {
const { t } = useTranslation();
const openPage = ReaderControls.useOpenPage();
const { readingDirection } = ReaderService.useSettings();
const getOptionForDirection = useGetOptionForDirection();
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
const direction = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection.value];
const direction = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
return (
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
@@ -73,3 +78,9 @@ export const ReaderNavBarDesktopPageNavigation = ({
</Stack>
);
};
export const ReaderNavBarDesktopPageNavigation = withPropsFrom(
BaseReaderNavBarDesktopPageNavigation,
[() => ({ openPage: ReaderControls.useOpenPage() }), ReaderService.useSettingsWithoutDefaultFlag],
['readingDirection', 'openPage'],
);

View File

@@ -25,11 +25,20 @@ import { ReaderChapterList } from '@/modules/reader/components/overlay/navigatio
import { ReaderBottomBarMobileQuickSettings } from '@/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobileQuickSettings.tsx';
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
import { ReaderStateChapters, TReaderScrollbarContext } from '@/modules/reader/types/Reader.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderBottomBarMobile = ({ openSettings, isVisible }: ReaderBottomBarMobileProps) => {
const BaseReaderBottomBarMobile = ({
openSettings,
isVisible,
currentChapter,
chapters,
scrollbarXSize,
scrollbarYSize,
}: ReaderBottomBarMobileProps &
Pick<ReaderStateChapters, 'currentChapter' | 'chapters'> &
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'>) => {
const { t } = useTranslation();
const { currentChapter, chapters } = useReaderStateChaptersContext();
const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext();
const chapterListPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-chapter-list-dialog' });
const quickSettingsPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-quick-settings-dialog' });
@@ -112,3 +121,9 @@ export const ReaderBottomBarMobile = ({ openSettings, isVisible }: ReaderBottomB
</>
);
};
export const ReaderBottomBarMobile = withPropsFrom(
BaseReaderBottomBarMobile,
[useReaderStateChaptersContext, useReaderScrollbarContext],
['currentChapter', 'chapters', 'scrollbarXSize', 'scrollbarYSize'],
);

View File

@@ -13,11 +13,16 @@ import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { DefaultSettingFootnote } from '@/modules/reader/components/settings/DefaultSettingFootnote.tsx';
import { IReaderSettingsWithDefaultFlag, TReaderStateMangaContext } from '@/modules/reader/types/Reader.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
const DEFAULT_MANGA: MangaIdInfo = { id: -1 };
export const ReaderBottomBarMobileQuickSettings = () => {
const { manga } = useReaderStateMangaContext();
const { readingMode, readingDirection } = ReaderService.useSettings();
const BaseReaderBottomBarMobileQuickSettings = ({
manga,
readingMode,
readingDirection,
}: Pick<TReaderStateMangaContext, 'manga'> &
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'readingDirection'>) => {
const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? DEFAULT_MANGA);
if (!manga) {
@@ -42,3 +47,9 @@ export const ReaderBottomBarMobileQuickSettings = () => {
</Stack>
);
};
export const ReaderBottomBarMobileQuickSettings = withPropsFrom(
BaseReaderBottomBarMobileQuickSettings,
[useReaderStateMangaContext, ReaderService.useSettings],
['manga', 'readingMode', 'readingDirection'],
);

View File

@@ -11,8 +11,7 @@ import { ReactNode, useCallback, useMemo, useRef } from 'react';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import { TypographyProps } from '@mui/material/Typography';
import { StackProps } from '@mui/material/Stack';
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
import { ReaderProgressBarProps } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { ReaderProgressBarProps, TReaderProgressBarContext } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { ReaderProgressBarPageNumber } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarPageNumber.tsx';
import { ReaderProgressBarContainer } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarContainer.tsx';
import { ReaderProgressBarRoot } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarRoot.tsx';
@@ -30,8 +29,10 @@ import { getOptionForDirection as getOptionForDirectionImpl } from '@/modules/th
import { ReaderProgressBarSlotsActionArea } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlotsActionArea.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ReaderControls } from '@/modules/reader/services/ReaderControls.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
export const ReaderProgressBar = ({
const BaseReaderProgressBar = ({
totalPages,
pages,
pageLoadStates,
@@ -40,34 +41,37 @@ export const ReaderProgressBar = ({
slots,
createProgressBarSlot,
progressBarPosition,
}: ReaderProgressBarProps & {
createProgressBarSlot: (
page: ReaderProgressBarProps['pages'][number],
pageLoadStates: ReaderProgressBarProps['pageLoadStates'],
pagesIndex: number,
) => ReactNode;
slotProps?: {
container?: StackProps;
progressBarRoot?: StackProps;
progressBarSlotsActionArea?: StackProps;
progressBarSlotsContainer?: StackProps;
progressBarSlot?: BoxProps;
progressBarReadPages?: BoxProps;
progressBarCurrentPageSlot?: BoxProps;
progressBarPageTexts?: {
base?: TypographyProps;
current?: TypographyProps;
total?: TypographyProps;
isDragging,
setIsDragging,
openPage,
direction,
}: ReaderProgressBarProps &
Pick<TReaderProgressBarContext, 'isDragging' | 'setIsDragging'> & {
createProgressBarSlot: (
page: ReaderProgressBarProps['pages'][number],
pageLoadStates: ReaderProgressBarProps['pageLoadStates'],
pagesIndex: number,
) => ReactNode;
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;
};
}) => {
const { isDragging, setIsDragging } = useReaderProgressBarContext();
const openPage = ReaderControls.useOpenPage();
const direction = ReaderService.useGetThemeDirection();
slots?: {
progressBarCurrentPage?: ReactNode;
};
openPage: ReturnType<typeof ReaderControls.useOpenPage>;
direction: ReturnType<typeof ReaderService.useGetThemeDirection>;
}) => {
const progressBarRef = useRef<HTMLDivElement | null>(null);
const isHorizontalPosition = getProgressBarPositionInfo(progressBarPosition).isHorizontal;
@@ -212,3 +216,15 @@ export const ReaderProgressBar = ({
</ReaderProgressBarContainer>
);
};
export const ReaderProgressBar = withPropsFrom(
BaseReaderProgressBar,
[
useReaderProgressBarContext,
() => ({ openPage: ReaderControls.useOpenPage() }),
() => ({
direction: ReaderService.useGetThemeDirection(),
}),
],
['isDragging', 'setIsDragging', 'openPage', 'direction'],
);

View File

@@ -16,9 +16,15 @@ import { ThemeMode } from '@/modules/theme/contexts/ThemeModeContext.tsx';
import { createTheme } from '@/modules/theme/services/ThemeCreator.ts';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { DIRECTION_TO_CACHE } from '@/modules/theme/ThemeDirectionCache.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderProgressBarDirectionWrapper = ({ children }: { children: ReactNode }) => {
const direction = ReaderService.useGetThemeDirection();
const BaseReaderProgressBarDirectionWrapper = ({
children,
direction,
}: {
children: ReactNode;
direction: ReturnType<typeof ReaderService.useGetThemeDirection>;
}) => {
const [appTheme] = useLocalStorage<AppThemes>('appTheme', 'default');
const [themeMode] = useLocalStorage<ThemeMode>('themeMode', ThemeMode.SYSTEM);
const [pureBlackMode] = useLocalStorage<boolean>('pureBlackMode', false);
@@ -40,3 +46,9 @@ export const ReaderProgressBarDirectionWrapper = ({ children }: { children: Reac
</CacheProvider>
);
};
export const ReaderProgressBarDirectionWrapper = withPropsFrom(
BaseReaderProgressBarDirectionWrapper,
[() => ({ direction: ReaderService.useGetThemeDirection() })],
['direction'],
);

View File

@@ -20,15 +20,22 @@ import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/R
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts';
import { ProgressBarPosition, ReaderResumeMode } from '@/modules/reader/types/Reader.types.ts';
import { ProgressBarPosition, ReaderResumeMode, ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
import { ReaderProgressBarDirectionWrapper } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx';
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.types.ts';
import { TReaderProgressBarContext } from '@/modules/reader/types/ReaderProgressBar.types.ts';
export const MobileReaderProgressBar = () => {
const { nextChapter, previousChapter } = useReaderStateChaptersContext();
const { isVisible } = useReaderOverlayContext();
const { setIsMaximized } = useReaderProgressBarContext();
const BaseMobileReaderProgressBar = ({
previousChapter,
nextChapter,
isVisible,
setIsMaximized,
}: Pick<ReaderStateChapters, 'previousChapter' | 'nextChapter'> &
Pick<TReaderOverlayContext, 'isVisible'> &
Pick<TReaderProgressBarContext, 'setIsMaximized'>) => {
const pagesState = userReaderStatePagesContext();
const { currentPageIndex, pages } = pagesState;
@@ -177,3 +184,9 @@ export const MobileReaderProgressBar = () => {
</ReaderProgressBarDirectionWrapper>
);
};
export const MobileReaderProgressBar = withPropsFrom(
BaseMobileReaderProgressBar,
[useReaderStateChaptersContext, useReaderOverlayContext, useReaderProgressBarContext],
['previousChapter', 'nextChapter', 'isVisible', 'setIsMaximized'],
);

View File

@@ -9,26 +9,40 @@
import { alpha, darken, lighten, useTheme } from '@mui/material/styles';
import Box from '@mui/material/Box';
import { ReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx';
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
import { ReaderProgressBarSlot } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarSlot.tsx';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ProgressBarType } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ProgressBarType, TReaderScrollbarContext } from '@/modules/reader/types/Reader.types.ts';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { getPage, getProgressBarPositionInfo } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { ReaderProgressBarDirectionWrapper } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx';
import { TReaderProgressBarContext } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
export const StandardReaderProgressBar = () => {
const BaseStandardReaderProgressBar = ({
readerNavBarWidth,
isMaximized,
setIsMaximized,
isDragging,
progressBarType,
progressBarSize,
progressBarPosition,
readerDirection,
scrollbarXSize,
scrollbarYSize,
}: Pick<NavbarContextType, 'readerNavBarWidth'> &
Pick<TReaderProgressBarContext, 'isMaximized' | 'setIsMaximized' | 'isDragging'> &
Pick<IReaderSettings, 'progressBarType' | 'progressBarSize' | 'progressBarPosition'> &
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> & {
readerDirection: ReturnType<typeof ReaderService.useGetThemeDirection>;
}) => {
const theme = useTheme();
const pagesState = userReaderStatePagesContext();
const { currentPageIndex, pages } = pagesState;
const { readerNavBarWidth } = useNavBarContext();
const { isMaximized, setIsMaximized, isDragging } = useReaderProgressBarContext();
const { progressBarType, progressBarSize, progressBarPosition } = ReaderService.useSettings();
const readerDirection = ReaderService.useGetThemeDirection();
const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext();
const currentPagesIndex = getPage(currentPageIndex, pages).pagesIndex;
@@ -255,3 +269,26 @@ export const StandardReaderProgressBar = () => {
</ReaderProgressBarDirectionWrapper>
);
};
export const StandardReaderProgressBar = withPropsFrom(
BaseStandardReaderProgressBar,
[
useNavBarContext,
useReaderProgressBarContext,
ReaderService.useSettingsWithoutDefaultFlag,
() => ({ readerDirection: ReaderService.useGetThemeDirection() }),
useReaderScrollbarContext,
],
[
'readerNavBarWidth',
'isMaximized',
'setIsMaximized',
'isDragging',
'progressBarType',
'progressBarSize',
'progressBarPosition',
'readerDirection',
'scrollbarXSize',
'scrollbarYSize',
],
);

View File

@@ -23,28 +23,30 @@ import { ReaderFilterSettings } from '@/modules/reader/components/settings/filte
import { ReaderBehaviourSettings } from '@/modules/reader/components/settings/behaviour/ReaderBehaviourSettings.tsx';
import { ReaderDefaultLayoutSettings } from '@/modules/reader/components/settings/layout/ReaderDefaultLayoutSettings.tsx';
import { ReaderHotkeysSettings } from '@/modules/reader/components/settings/hotkeys/ReaderHotkeysSettings.tsx';
import { TReaderTapZoneContext } from '@/modules/reader/types/TapZoneLayout.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderSettingsTabs = ({
const BaseReaderSettingsTabs = ({
activeTab,
setActiveTab,
areDefaultSettings,
settings,
updateSetting,
deleteSetting,
}: {
activeTab: number;
setActiveTab: (tab: number) => void;
settings: IReaderSettingsWithDefaultFlag;
updateSetting: (...args: OmitFirst<Parameters<typeof ReaderService.updateSetting>>) => void;
areDefaultSettings?: boolean;
deleteSetting: (setting: keyof IReaderSettings) => void;
}) => {
setShowPreview,
mode: overlayMode,
}: 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;
}) => {
const { t } = useTranslation();
const { setShowPreview } = useReaderTapZoneContext();
const isTouchDevice = MediaQuery.useIsTouchDevice();
const { mode: overlayMode } = ReaderService.useOverlayMode();
return (
<>
<TabsMenu
@@ -177,3 +179,9 @@ export const ReaderSettingsTabs = ({
</>
);
};
export const ReaderSettingsTabs = withPropsFrom(
BaseReaderSettingsTabs,
[useReaderTapZoneContext, ReaderService.useOverlayMode],
['setShowPreview', 'mode'],
);

View File

@@ -9,7 +9,7 @@
import { ComponentProps, ForwardedRef, forwardRef } from 'react';
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ReaderCustomFilter } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderCustomFilter, TReaderScrollbarContext } from '@/modules/reader/types/Reader.types.ts';
import {
getImageMarginStyling,
getImagePlaceholderStyling,
@@ -18,6 +18,7 @@ import {
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrollbarContext.tsx';
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
const getCustomFilterString = (customFilter: ReaderCustomFilter): string =>
Object.keys(customFilter)
@@ -48,7 +49,7 @@ const getCustomFilterString = (customFilter: ReaderCustomFilter): string =>
})
.join(' ');
export const ReaderPage = forwardRef(
const BaseReaderPage = forwardRef(
(
{
display,
@@ -56,18 +57,27 @@ export const ReaderPage = forwardRef(
position,
marginTop,
shouldLoad,
readingMode,
customFilter,
pageScaleMode,
shouldStretchPage,
readerWidth,
scrollbarXSize,
scrollbarYSize,
...props
}: Omit<ComponentProps<typeof SpinnerImage>, 'ref' | 'spinnerStyle' | 'imgStyle'> & {
display: boolean;
doublePage?: boolean;
position?: 'left' | 'right';
marginTop?: number;
},
}: Omit<ComponentProps<typeof SpinnerImage>, 'ref' | 'spinnerStyle' | 'imgStyle'> &
Pick<
IReaderSettings,
'readingMode' | 'customFilter' | 'pageScaleMode' | 'shouldStretchPage' | 'readerWidth'
> &
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> & {
display: boolean;
doublePage?: boolean;
position?: 'left' | 'right';
marginTop?: number;
},
ref: ForwardedRef<HTMLImageElement | null>,
) => {
const { readingMode, customFilter, pageScaleMode, shouldStretchPage, readerWidth } =
ReaderService.useSettings();
const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext();
const isTabletWidth = MediaQuery.useIsTabletWidth();
if (!display && !shouldLoad) {
@@ -83,10 +93,10 @@ export const ReaderPage = forwardRef(
spinnerStyle={{
backgroundColor: 'background.paper',
...getImagePlaceholderStyling(
readingMode.value,
shouldStretchPage.value,
pageScaleMode.value,
readerWidth.value,
readingMode,
shouldStretchPage,
pageScaleMode,
readerWidth,
scrollbarXSize,
scrollbarYSize,
doublePage,
@@ -99,11 +109,11 @@ export const ReaderPage = forwardRef(
}}
imgStyle={{
...getImageWidthStyling(
readingMode.value,
shouldStretchPage.value,
pageScaleMode.value,
readingMode,
shouldStretchPage,
pageScaleMode,
doublePage,
readerWidth.value,
readerWidth,
true,
),
display: 'block',
@@ -130,3 +140,17 @@ export const ReaderPage = forwardRef(
);
},
);
export const ReaderPage = withPropsFrom(
BaseReaderPage,
[ReaderService.useSettingsWithoutDefaultFlag, useReaderScrollbarContext],
[
'readingMode',
'customFilter',
'pageScaleMode',
'shouldStretchPage',
'readerWidth',
'scrollbarXSize',
'scrollbarYSize',
],
);

View File

@@ -18,12 +18,14 @@ import { TChapterReader } from '@/modules/chapter/Chapter.types.ts';
import {
IReaderSettings,
ReaderPageScaleMode,
ReaderStateChapters,
ReaderTransitionPageMode,
ReadingMode,
TReaderScrollbarContext,
TReaderStateMangaContext,
} from '@/modules/reader/types/Reader.types.ts';
import { isTransitionPageVisible } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import {
isContinuousReadingMode,
@@ -31,6 +33,8 @@ import {
} from '@/modules/reader/utils/ReaderSettings.utils.tsx';
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
const ChapterInfo = ({
title,
@@ -58,22 +62,29 @@ const ChapterInfo = ({
);
};
export const ReaderTransitionPage = ({
const BaseReaderTransitionPage = ({
type,
mode,
readingMode,
pageScaleMode,
}: Pick<IReaderSettings, 'readingMode' | 'pageScaleMode'> & {
type: Exclude<ReaderTransitionPageMode, ReaderTransitionPageMode.NONE | ReaderTransitionPageMode.BOTH>;
mode: ReaderTransitionPageMode;
}) => {
manga,
currentChapter,
previousChapter,
nextChapter,
scrollbarXSize,
scrollbarYSize,
readerNavBarWidth,
}: Pick<IReaderSettings, 'readingMode' | 'pageScaleMode'> &
Pick<TReaderStateMangaContext, 'manga'> &
Pick<ReaderStateChapters, 'currentChapter' | 'previousChapter' | 'nextChapter'> &
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> &
Pick<NavbarContextType, 'readerNavBarWidth'> & {
type: Exclude<ReaderTransitionPageMode, ReaderTransitionPageMode.NONE | ReaderTransitionPageMode.BOTH>;
mode: ReaderTransitionPageMode;
}) => {
const { t } = useTranslation();
const handleBack = useBackButton();
const { manga } = useReaderStateMangaContext();
const { currentChapter, nextChapter, previousChapter } = useReaderStateChaptersContext();
const { scrollbarXSize, scrollbarYSize } = useReaderScrollbarContext();
const { readerNavBarWidth } = useNavBarContext();
const isPreviousType = type === ReaderTransitionPageMode.PREVIOUS;
const isNextType = type === ReaderTransitionPageMode.NEXT;
@@ -191,3 +202,17 @@ export const ReaderTransitionPage = ({
</Stack>
);
};
export const ReaderTransitionPage = withPropsFrom(
BaseReaderTransitionPage,
[useReaderStateChaptersContext, useReaderScrollbarContext, useNavBarContext],
[
'manga',
'currentChapter',
'previousChapter',
'nextChapter',
'scrollbarXSize',
'scrollbarYSize',
'readerNavBarWidth',
],
);

View File

@@ -19,7 +19,13 @@ import {
import Stack from '@mui/material/Stack';
import { useTheme } from '@mui/material/styles';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { PageInViewportType, ReadingDirection, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import {
IReaderSettings,
PageInViewportType,
ReadingDirection,
ReadingMode,
TReaderScrollbarContext,
} from '@/modules/reader/types/Reader.types.ts';
import { ReaderPagedPager } from '@/modules/reader/components/viewer/pager/ReaderPagedPager.tsx';
import { ReaderDoublePagedPager } from '@/modules/reader/components/viewer/pager/ReaderDoublePagedPager.tsx';
import { ReaderHorizontalPager } from '@/modules/reader/components/viewer/pager/ReaderHorizontalPager.tsx';
@@ -43,6 +49,9 @@ import { useMouseDragScroll } from '@/modules/core/hooks/useMouseDragScroll.tsx'
import { DirectionOffset } from '@/Base.types.ts';
import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.types.ts';
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType> = {
[ReadingMode.SINGLE_PAGE]: PageInViewportType.X,
@@ -52,251 +61,308 @@ const READING_MODE_TO_IN_VIEWPORT_TYPE: Record<ReadingMode, PageInViewportType>
[ReadingMode.WEBTOON]: PageInViewportType.Y,
};
export const ReaderViewer = forwardRef((_, ref: ForwardedRef<HTMLDivElement | null>) => {
const {
currentPageIndex,
pageToScrollToIndex,
pages,
setPages,
totalPages,
pageUrls,
pageLoadStates,
setPageLoadStates,
transitionPageMode,
retryFailedPagesKeyPrefix,
} = userReaderStatePagesContext();
const { direction: themeDirection } = useTheme();
const { readingMode, shouldOffsetDoubleSpreads, readingDirection } = ReaderService.useSettings();
const { setScrollbarXSize, setScrollbarYSize } = useReaderScrollbarContext();
const { isVisible: isOverlayVisible, setIsVisible: setIsOverlayVisible } = useReaderOverlayContext();
const updateCurrentPageIndex = ReaderControls.useUpdateCurrentPageIndex();
const BaseReaderViewer = forwardRef(
(
{
currentPageIndex,
pageToScrollToIndex,
pages,
setPages,
totalPages,
pageUrls,
pageLoadStates,
setPageLoadStates,
transitionPageMode,
retryFailedPagesKeyPrefix,
readingMode,
shouldOffsetDoubleSpreads,
readingDirection,
setScrollbarXSize,
setScrollbarYSize,
isVisible: isOverlayVisible,
setIsVisible: setIsOverlayVisible,
updateCurrentPageIndex,
}: Pick<
ReaderStatePages,
| 'currentPageIndex'
| 'pageToScrollToIndex'
| 'pages'
| 'setPages'
| 'totalPages'
| 'pageUrls'
| 'pageLoadStates'
| 'setPageLoadStates'
| 'transitionPageMode'
| 'retryFailedPagesKeyPrefix'
> &
Pick<IReaderSettings, 'readingMode' | 'shouldOffsetDoubleSpreads' | 'readingDirection'> &
Pick<TReaderScrollbarContext, 'setScrollbarXSize' | 'setScrollbarYSize'> &
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> & {
updateCurrentPageIndex: ReturnType<typeof ReaderControls.useUpdateCurrentPageIndex>;
},
const scrollElementRef = useRef<HTMLDivElement | null>(null);
useImperativeHandle(ref, () => scrollElementRef.current!);
ref: ForwardedRef<HTMLDivElement | null>,
) => {
const { direction: themeDirection } = useTheme();
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode.value);
const isDragging = useMouseDragScroll(isContinuousReadingModeActive ? scrollElementRef : undefined);
const scrollElementRef = useRef<HTMLDivElement | null>(null);
useImperativeHandle(ref, () => scrollElementRef.current!);
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
useLayoutEffect(() => {
setScrollbarXSize(scrollbarXSize);
setScrollbarYSize(scrollbarYSize);
}, [scrollbarXSize, scrollbarYSize]);
const isContinuousReadingModeActive = isContinuousReadingMode(readingMode);
const isDragging = useMouseDragScroll(isContinuousReadingModeActive ? scrollElementRef : undefined);
const handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
const scrollbarXSize = MediaQuery.useGetScrollbarSize('width', scrollElementRef.current);
const scrollbarYSize = MediaQuery.useGetScrollbarSize('height', scrollElementRef.current);
useLayoutEffect(() => {
setScrollbarXSize(scrollbarXSize);
setScrollbarYSize(scrollbarYSize);
}, [scrollbarXSize, scrollbarYSize]);
const [wasDoublePageMode, setWasDoublePageMode] = useState(readingMode.value === ReadingMode.DOUBLE_PAGE);
const [pagesToSpreadState, setPagesToSpreadState] = useState(Array(totalPages).fill(false));
const handleClick = ReaderControls.useHandleClick(scrollElementRef.current);
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
const [wasDoublePageMode, setWasDoublePageMode] = useState(readingMode === ReadingMode.DOUBLE_PAGE);
const [pagesToSpreadState, setPagesToSpreadState] = useState(Array(totalPages).fill(false));
const imageRefs = useRef<(HTMLElement | null)[]>(pages.map(() => null));
const actualPages = useMemo(() => {
const arePagesLoaded = pageUrls.length;
if (!arePagesLoaded) {
return pages;
}
if (readingMode === ReadingMode.DOUBLE_PAGE) {
return getDoublePageModePages(
pageUrls,
pagesToSpreadState,
shouldOffsetDoubleSpreads,
readingDirection,
);
}
const actualPages = useMemo(() => {
const arePagesLoaded = pageUrls.length;
if (!arePagesLoaded) {
return pages;
}
}, [pagesToSpreadState, readingMode, shouldOffsetDoubleSpreads, readingDirection]);
if (readingMode.value === ReadingMode.DOUBLE_PAGE) {
return getDoublePageModePages(
pageUrls,
pagesToSpreadState,
shouldOffsetDoubleSpreads.value,
readingDirection.value,
);
}
const Pager = useMemo(() => {
switch (readingMode) {
case ReadingMode.SINGLE_PAGE:
return ReaderPagedPager;
case ReadingMode.DOUBLE_PAGE:
return ReaderDoublePagedPager;
case ReadingMode.CONTINUOUS_VERTICAL:
case ReadingMode.WEBTOON:
return ReaderVerticalPager;
case ReadingMode.CONTINUOUS_HORIZONTAL:
return ReaderHorizontalPager;
default:
throw new Error(`Unexpected "ReadingMode" (${readingMode})`);
}
}, [readingMode]);
return pages;
}, [pagesToSpreadState, readingMode.value, shouldOffsetDoubleSpreads.value, readingDirection.value]);
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode];
const Pager = useMemo(() => {
switch (readingMode.value) {
case ReadingMode.SINGLE_PAGE:
return ReaderPagedPager;
case ReadingMode.DOUBLE_PAGE:
return ReaderDoublePagedPager;
case ReadingMode.CONTINUOUS_VERTICAL:
case ReadingMode.WEBTOON:
return ReaderVerticalPager;
case ReadingMode.CONTINUOUS_HORIZONTAL:
return ReaderHorizontalPager;
default:
throw new Error(`Unexpected "ReadingMode" (${readingMode.value})`);
}
}, [readingMode.value]);
const inViewportType = READING_MODE_TO_IN_VIEWPORT_TYPE[readingMode.value];
// reset spread state
useLayoutEffect(() => {
setPagesToSpreadState(Array(totalPages).fill(false));
}, [totalPages]);
// optionally convert pages to normal or double page mode
useLayoutEffect(() => {
const convertPagesToNormalPageMode = wasDoublePageMode && readingMode.value !== ReadingMode.DOUBLE_PAGE;
if (convertPagesToNormalPageMode) {
setWasDoublePageMode(false);
setPages(createPagesData(pageUrls));
// reset spread state
useLayoutEffect(() => {
setPagesToSpreadState(Array(totalPages).fill(false));
return;
}
}, [totalPages]);
const convertPagesToDoublePageMode = readingMode.value === ReadingMode.DOUBLE_PAGE;
if (convertPagesToDoublePageMode) {
if (!wasDoublePageMode) {
updateCurrentPageIndex(getNextIndexFromPage(getPage(currentPageIndex, actualPages)));
}
setPages(actualPages);
setWasDoublePageMode(readingMode.value === ReadingMode.DOUBLE_PAGE);
}
}, [actualPages, readingMode.value]);
// handle user page selection
useLayoutEffect(() => {
const pageToScrollTo = getPage(pageToScrollToIndex, pages);
if (isContinuousReadingModeActive) {
const directionOffset =
pageToScrollToIndex > currentPageIndex ? DirectionOffset.PREVIOUS : DirectionOffset.NEXT;
const imageRef = imageRefs.current[pageToScrollTo.pagesIndex];
imageRef?.scrollIntoView({
block: 'start',
inline: getScrollIntoViewInlineOption(directionOffset, themeDirection, readingDirection.value),
});
}
const newPageIndex = getNextIndexFromPage(pageToScrollTo);
const isLastPage = newPageIndex === totalPages - 1;
updateCurrentPageIndex(newPageIndex, !isLastPage);
}, [pageToScrollToIndex]);
// hide cursor on mouse inactivity
const mouseInactiveTimeout = useRef<NodeJS.Timeout>();
useEffect(() => {
const setCursorVisibility = (visible: boolean) => {
const scrollElement = scrollElementRef.current;
if (!scrollElement) {
// optionally convert pages to normal or double page mode
useLayoutEffect(() => {
const convertPagesToNormalPageMode = wasDoublePageMode && readingMode !== ReadingMode.DOUBLE_PAGE;
if (convertPagesToNormalPageMode) {
setWasDoublePageMode(false);
setPages(createPagesData(pageUrls));
setPagesToSpreadState(Array(totalPages).fill(false));
return;
}
scrollElement.style.cursor = visible ? 'default' : 'none';
};
const convertPagesToDoublePageMode = readingMode === ReadingMode.DOUBLE_PAGE;
if (convertPagesToDoublePageMode) {
if (!wasDoublePageMode) {
updateCurrentPageIndex(getNextIndexFromPage(getPage(currentPageIndex, actualPages)));
}
setPages(actualPages);
setWasDoublePageMode(readingMode === ReadingMode.DOUBLE_PAGE);
}
}, [actualPages, readingMode]);
const handleMouseMove = () => {
setCursorVisibility(true);
clearTimeout(mouseInactiveTimeout.current);
mouseInactiveTimeout.current = setTimeout(() => {
setCursorVisibility(false);
}, 5000);
};
// handle user page selection
useLayoutEffect(() => {
const pageToScrollTo = getPage(pageToScrollToIndex, pages);
handleMouseMove();
window.addEventListener('mousemove', handleMouseMove);
return () => {
setCursorVisibility(true);
window.removeEventListener('mousemove', handleMouseMove);
clearTimeout(mouseInactiveTimeout.current);
};
}, []);
if (isContinuousReadingModeActive) {
const directionOffset =
pageToScrollToIndex > currentPageIndex ? DirectionOffset.PREVIOUS : DirectionOffset.NEXT;
const imageRef = imageRefs.current[pageToScrollTo.pagesIndex];
// invert x and y scrolling for the continuous horizontal reading mode
useEffect(() => {
if (readingMode.value !== ReadingMode.CONTINUOUS_HORIZONTAL) {
return () => {};
}
if (!scrollElementRef.current) {
return () => {};
}
const handleScroll = (e: WheelEvent) => {
e.preventDefault();
if (e.shiftKey) {
scrollElementRef.current?.scrollBy({
top: e.deltaY,
imageRef?.scrollIntoView({
block: 'start',
inline: getScrollIntoViewInlineOption(directionOffset, themeDirection, readingDirection),
});
return;
}
scrollElementRef.current?.scrollBy({
left: readingDirection.value === ReadingDirection.LTR ? e.deltaY : e.deltaY * -1,
});
};
const newPageIndex = getNextIndexFromPage(pageToScrollTo);
const isLastPage = newPageIndex === totalPages - 1;
scrollElementRef.current.addEventListener('wheel', handleScroll);
return () => scrollElementRef.current?.removeEventListener('wheel', handleScroll);
}, [readingMode.value, readingDirection.value]);
updateCurrentPageIndex(newPageIndex, !isLastPage);
}, [pageToScrollToIndex]);
// hide overlay on user triggered scroll
useEffect(() => {
const handleScroll = () => {
if (isOverlayVisible) {
setIsOverlayVisible(false);
// hide cursor on mouse inactivity
const mouseInactiveTimeout = useRef<NodeJS.Timeout>();
useEffect(() => {
const setCursorVisibility = (visible: boolean) => {
const scrollElement = scrollElementRef.current;
if (!scrollElement) {
return;
}
scrollElement.style.cursor = visible ? 'default' : 'none';
};
const handleMouseMove = () => {
setCursorVisibility(true);
clearTimeout(mouseInactiveTimeout.current);
mouseInactiveTimeout.current = setTimeout(() => {
setCursorVisibility(false);
}, 5000);
};
handleMouseMove();
window.addEventListener('mousemove', handleMouseMove);
return () => {
setCursorVisibility(true);
window.removeEventListener('mousemove', handleMouseMove);
clearTimeout(mouseInactiveTimeout.current);
};
}, []);
// invert x and y scrolling for the continuous horizontal reading mode
useEffect(() => {
if (readingMode !== ReadingMode.CONTINUOUS_HORIZONTAL) {
return () => {};
}
};
scrollElementRef.current?.addEventListener('wheel', handleScroll);
scrollElementRef.current?.addEventListener('touchmove', handleScroll);
return () => {
scrollElementRef.current?.removeEventListener('wheel', handleScroll);
scrollElementRef.current?.removeEventListener('touchmove', handleScroll);
};
}, [isOverlayVisible]);
return (
<Stack
ref={scrollElementRef}
sx={{
width: '100%',
height: '100%',
overflow: 'auto',
...applyStyles(isContinuousVerticalReadingMode(readingMode.value), { alignItems: 'center' }),
}}
onClick={(e) => !isDragging && handleClick(e)}
onScroll={() =>
ReaderControls.updateCurrentPageOnScroll(
imageRefs,
totalPages - 1,
updateCurrentPageIndex,
inViewportType,
readingDirection.value,
)
if (!scrollElementRef.current) {
return () => {};
}
>
<Pager
totalPages={totalPages}
currentPageIndex={currentPageIndex}
pages={actualPages}
transitionPageMode={transitionPageMode}
pageLoadStates={pageLoadStates}
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
imageRefs={imageRefs}
onLoad={(pagesIndex, isPrimary = true) => {
const page = actualPages[pagesIndex];
const { index, url } = isPrimary ? page.primary : page.secondary!;
if (readingMode.value === ReadingMode.DOUBLE_PAGE) {
const img = new Image();
img.onload = () => {
setPagesToSpreadState((prevState) => prevState.toSpliced(index, 1, isSpreadPage(img)));
};
img.src = url;
}
const handleScroll = (e: WheelEvent) => {
e.preventDefault();
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(index, 1, { loaded: true }),
);
if (e.shiftKey) {
scrollElementRef.current?.scrollBy({
top: e.deltaY,
});
return;
}
scrollElementRef.current?.scrollBy({
left: readingDirection === ReadingDirection.LTR ? e.deltaY : e.deltaY * -1,
});
};
scrollElementRef.current.addEventListener('wheel', handleScroll);
return () => scrollElementRef.current?.removeEventListener('wheel', handleScroll);
}, [readingMode, readingDirection]);
// hide overlay on user triggered scroll
useEffect(() => {
const handleScroll = () => {
if (isOverlayVisible) {
setIsOverlayVisible(false);
}
};
scrollElementRef.current?.addEventListener('wheel', handleScroll);
scrollElementRef.current?.addEventListener('touchmove', handleScroll);
return () => {
scrollElementRef.current?.removeEventListener('wheel', handleScroll);
scrollElementRef.current?.removeEventListener('touchmove', handleScroll);
};
}, [isOverlayVisible]);
return (
<Stack
ref={scrollElementRef}
sx={{
width: '100%',
height: '100%',
overflow: 'auto',
...applyStyles(isContinuousVerticalReadingMode(readingMode), { alignItems: 'center' }),
}}
onError={(pageIndex) => {
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(pageIndex, 1, { loaded: false, error: true }),
);
}}
/>
</Stack>
);
});
onClick={(e) => !isDragging && handleClick(e)}
onScroll={() =>
ReaderControls.updateCurrentPageOnScroll(
imageRefs,
totalPages - 1,
updateCurrentPageIndex,
inViewportType,
readingDirection,
)
}
>
<Pager
totalPages={totalPages}
currentPageIndex={currentPageIndex}
pages={actualPages}
transitionPageMode={transitionPageMode}
pageLoadStates={pageLoadStates}
retryFailedPagesKeyPrefix={retryFailedPagesKeyPrefix}
imageRefs={imageRefs}
onLoad={(pagesIndex, isPrimary = true) => {
const page = actualPages[pagesIndex];
const { index, url } = isPrimary ? page.primary : page.secondary!;
if (readingMode === ReadingMode.DOUBLE_PAGE) {
const img = new Image();
img.onload = () => {
setPagesToSpreadState((prevState) => prevState.toSpliced(index, 1, isSpreadPage(img)));
};
img.src = url;
}
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(index, 1, { loaded: true }),
);
}}
onError={(pageIndex) => {
setPageLoadStates((statePageLoadStates) =>
statePageLoadStates.toSpliced(pageIndex, 1, { loaded: false, error: true }),
);
}}
/>
</Stack>
);
},
);
export const ReaderViewer = withPropsFrom(
BaseReaderViewer,
[
userReaderStatePagesContext,
ReaderService.useSettingsWithoutDefaultFlag,
useReaderScrollbarContext,
useReaderOverlayContext,
() => ({ updateCurrentPageIndex: ReaderControls.useUpdateCurrentPageIndex() }),
],
[
'currentPageIndex',
'pageToScrollToIndex',
'pages',
'setPages',
'totalPages',
'pageUrls',
'pageLoadStates',
'setPageLoadStates',
'transitionPageMode',
'retryFailedPagesKeyPrefix',
'readingMode',
'shouldOffsetDoubleSpreads',
'readingDirection',
'setScrollbarXSize',
'setScrollbarYSize',
'isVisible',
'setIsVisible',
'updateCurrentPageIndex',
],
);

View File

@@ -11,29 +11,36 @@ import Box, { BoxProps } from '@mui/material/Box';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { getImageWidthStyling, getPageIndexesToLoad } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { ReaderPagerProps, ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderPagerProps, ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts';
import { ReaderTransitionPage } from '@/modules/reader/components/viewer/ReaderTransitionPage.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const BasePager = ({
const BaseBasePager = ({
currentPageIndex,
pages,
transitionPageMode,
imageRefs,
createPage,
slots,
}: Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> & {
createPage: (
page: ReaderStatePages['pages'][number],
pagesIndex: number,
shouldLoad: boolean,
shouldDisplay: boolean,
setRef: (element: HTMLElement | null) => void,
) => ReactNode;
slots?: { boxProps?: BoxProps };
}) => {
const { readingMode, pageScaleMode, shouldStretchPage, readerWidth, imagePreLoadAmount } =
ReaderService.useSettings();
readingMode,
pageScaleMode,
shouldStretchPage,
readerWidth,
imagePreLoadAmount,
}: Omit<ReaderPagerProps, 'pageLoadStates' | 'retryFailedPagesKeyPrefix'> &
Pick<
IReaderSettings,
'readingMode' | 'pageScaleMode' | 'shouldStretchPage' | 'readerWidth' | 'imagePreLoadAmount'
> & {
createPage: (
page: ReaderStatePages['pages'][number],
pagesIndex: number,
shouldLoad: boolean,
shouldDisplay: boolean,
setRef: (element: HTMLElement | null) => void,
) => ReactNode;
slots?: { boxProps?: BoxProps };
}) => {
const previousCurrentPageIndex = useRef(-1);
const pagesIndexesToRender = useMemo(
() => getPageIndexesToLoad(currentPageIndex, pages, previousCurrentPageIndex.current, imagePreLoadAmount),
@@ -48,20 +55,14 @@ export const BasePager = ({
{...slots?.boxProps}
sx={[
...(Array.isArray(slots?.boxProps?.sx) ? (slots?.boxProps?.sx ?? []) : [slots?.boxProps?.sx]),
getImageWidthStyling(
readingMode.value,
shouldStretchPage.value,
pageScaleMode.value,
false,
readerWidth.value,
),
getImageWidthStyling(readingMode, shouldStretchPage, pageScaleMode, false, readerWidth),
]}
>
<ReaderTransitionPage
type={ReaderTransitionPageMode.PREVIOUS}
mode={transitionPageMode}
readingMode={readingMode.value}
pageScaleMode={pageScaleMode.value}
readingMode={readingMode}
pageScaleMode={pageScaleMode}
/>
{pages.map((page, pagesIndex) =>
createPage(
@@ -78,9 +79,15 @@ export const BasePager = ({
<ReaderTransitionPage
type={ReaderTransitionPageMode.NEXT}
mode={transitionPageMode}
readingMode={readingMode.value}
pageScaleMode={pageScaleMode.value}
readingMode={readingMode}
pageScaleMode={pageScaleMode}
/>
</Box>
);
};
export const BasePager = withPropsFrom(
BaseBasePager,
[ReaderService.useSettingsWithoutDefaultFlag],
['readingMode', 'pageScaleMode', 'shouldStretchPage', 'readerWidth', 'imagePreLoadAmount'],
);

View File

@@ -10,10 +10,11 @@ import { Direction, useTheme } from '@mui/material/styles';
import { Fragment, useMemo } from 'react';
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { ReaderPagerProps, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { getNextIndexFromPage, getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
const getPagePosition = (
pageType: 'first' | 'second',
@@ -37,20 +38,20 @@ const getPagePosition = (
return isLtrReadingDirection ? 'right' : 'left';
};
export const ReaderDoublePagedPager = ({
const BaseReaderDoublePagedPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
readingDirection,
...props
}: ReaderPagerProps) => {
}: ReaderPagerProps & Pick<IReaderSettings, 'readingDirection'>) => {
const { currentPageIndex, pages, totalPages } = props;
const { readingDirection } = ReaderService.useSettings();
const { direction: themeDirection } = useTheme();
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
const isLtrReadingDirection = readingDirection.value === ReadingDirection.LTR;
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
return (
<BasePager
@@ -75,9 +76,7 @@ export const ReaderDoublePagedPager = ({
currentPage.primary.index,
totalPages,
pageLoadStates[primary.index].error ? retryFailedPagesKeyPrefix : undefined,
hasSecondaryPage
? getPagePosition('first', themeDirection, readingDirection.value)
: undefined,
hasSecondaryPage ? getPagePosition('first', themeDirection, readingDirection) : undefined,
hasSecondaryPage,
)}
{hasSecondaryPage &&
@@ -90,7 +89,7 @@ export const ReaderDoublePagedPager = ({
currentSecondaryPageIndex,
totalPages,
pageLoadStates[secondary.index].error ? retryFailedPagesKeyPrefix : undefined,
getPagePosition('second', themeDirection, readingDirection.value),
getPagePosition('second', themeDirection, readingDirection),
true,
)}
</Fragment>
@@ -115,3 +114,9 @@ export const ReaderDoublePagedPager = ({
/>
);
};
export const ReaderDoublePagedPager = withPropsFrom(
BaseReaderDoublePagedPager,
[ReaderService.useSettingsWithoutDefaultFlag],
['readingDirection'],
);

View File

@@ -10,22 +10,24 @@ import { useTheme } from '@mui/material/styles';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { ReaderPagerProps, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderPagerProps, ReadingDirection } from '@/modules/reader/types/Reader.types.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderHorizontalPager = ({
const BaseReaderHorizontalPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
pageGap,
readingDirection,
...props
}: ReaderPagerProps) => {
}: ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingDirection'>) => {
const { currentPageIndex, totalPages } = props;
const { pageGap, readingDirection } = ReaderService.useSettings();
const { direction: themeDirection } = useTheme();
const isLtrReadingDirection = readingDirection.value === ReadingDirection.LTR;
const isLtrReadingDirection = readingDirection === ReadingDirection.LTR;
return (
<BasePager
@@ -53,7 +55,7 @@ export const ReaderHorizontalPager = ({
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
gap: `${pageGap.value}px`,
gap: `${pageGap}px`,
...applyStyles(themeDirection === 'ltr', {
flexDirection: isLtrReadingDirection ? 'row' : 'row-reverse',
justifyContent: isLtrReadingDirection ? 'flex-start' : 'flex-end',
@@ -68,3 +70,9 @@ export const ReaderHorizontalPager = ({
/>
);
};
export const ReaderHorizontalPager = withPropsFrom(
BaseReaderHorizontalPager,
[ReaderService.useSettingsWithoutDefaultFlag],
['pageGap', 'readingDirection'],
);

View File

@@ -7,22 +7,24 @@
*/
import { BasePager } from '@/modules/reader/components/viewer/pager/BasePager.tsx';
import { ReaderPagerProps, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import { IReaderSettings, ReaderPagerProps, ReadingMode } from '@/modules/reader/types/Reader.types.ts';
import { createReaderPage } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
export const ReaderVerticalPager = ({
const BaseReaderVerticalPager = ({
onLoad,
onError,
pageLoadStates,
retryFailedPagesKeyPrefix,
readingMode,
pageGap,
...props
}: ReaderPagerProps) => {
}: ReaderPagerProps & Pick<IReaderSettings, 'pageGap' | 'readingMode'>) => {
const { currentPageIndex, totalPages } = props;
const { readingMode, pageGap } = ReaderService.useSettings();
const isWebtoonMode = readingMode.value === ReadingMode.WEBTOON;
const actualPageGap = isWebtoonMode ? 0 : pageGap.value;
const isWebtoonMode = readingMode === ReadingMode.WEBTOON;
const actualPageGap = isWebtoonMode ? 0 : pageGap;
return (
<BasePager
@@ -46,3 +48,9 @@ export const ReaderVerticalPager = ({
/>
);
};
export const ReaderVerticalPager = withPropsFrom(
BaseReaderVerticalPager,
[ReaderService.useSettingsWithoutDefaultFlag],
['pageGap', 'readingMode'],
);

View File

@@ -7,7 +7,7 @@
*/
import Box from '@mui/material/Box';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
@@ -24,7 +24,6 @@ import { LoadingPlaceholder } from '@/modules/core/components/placeholder/Loadin
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
import { GET_CHAPTERS_READER } from '@/lib/graphql/queries/ChapterQuery.ts';
import { Chapters } from '@/modules/chapter/services/Chapters.ts';
import { DirectionOffset } from '@/Base.types.ts';
@@ -38,38 +37,72 @@ import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { READER_BACKGROUND_TO_COLOR } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { createPageData, createPagesData } from '@/modules/reader/utils/ReaderPager.utils.tsx';
import { ReaderHotkeys } from '@/modules/reader/components/ReaderHotkeys.tsx';
import { ReaderResumeMode, ReaderTransitionPageMode } from '@/modules/reader/types/Reader.types.ts';
import {
IReaderSettings,
ReaderResumeMode,
ReaderStateChapters,
ReaderTransitionPageMode,
TReaderStateMangaContext,
TReaderStateSettingsContext,
} from '@/modules/reader/types/Reader.types.ts';
import { getInitialReaderPageIndex } from '@/modules/reader/utils/Reader.utils.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { NavbarContextType } from '@/modules/navigation-bar/NavigationBar.types.ts';
import { TReaderOverlayContext } from '@/modules/reader/types/ReaderOverlay.types.ts';
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
export const Reader = () => {
const BaseReader = ({
setTitle,
setOverride,
readerNavBarWidth,
isVisible: isOverlayVisible,
setIsVisible: setIsOverlayVisible,
manga,
setManga,
shouldSkipDupChapters,
backgroundColor,
setSettings,
initialChapter,
currentChapter,
chapters,
setReaderStateChapters,
firstPageUrl,
totalPages,
setTotalPages,
setCurrentPageIndex,
setPageToScrollToIndex,
setPages,
setPageUrls,
setPageLoadStates,
setTransitionPageMode,
}: Pick<NavbarContextType, 'setTitle' | 'setOverride' | 'readerNavBarWidth'> &
Pick<TReaderOverlayContext, 'isVisible' | 'setIsVisible'> &
Pick<TReaderStateMangaContext, 'manga' | 'setManga'> &
Pick<TReaderStateSettingsContext, 'setSettings'> &
Pick<IReaderSettings, 'shouldSkipDupChapters' | 'backgroundColor'> &
Pick<ReaderStateChapters, 'initialChapter' | 'currentChapter' | 'chapters' | 'setReaderStateChapters'> &
Pick<
ReaderStatePages,
| 'totalPages'
| 'setTotalPages'
| 'setCurrentPageIndex'
| 'setPageToScrollToIndex'
| 'setPages'
| 'setPageUrls'
| 'setPageLoadStates'
| 'setTransitionPageMode'
> & {
firstPageUrl?: string;
}) => {
const { t } = useTranslation();
const { setTitle, setOverride, readerNavBarWidth } = useNavBarContext();
const { isVisible: isOverlayVisible, setIsVisible: setIsOverlayVisible } = useReaderOverlayContext();
const { manga, setManga } = useReaderStateMangaContext();
const {
settings: { shouldSkipDupChapters },
setSettings,
} = useReaderStateSettingsContext();
const { initialChapter, currentChapter, chapters, setReaderStateChapters } = useReaderStateChaptersContext();
const {
setTotalPages,
setCurrentPageIndex,
setPageToScrollToIndex,
pages,
setPages,
setPageUrls,
setPageLoadStates,
setTransitionPageMode,
} = userReaderStatePagesContext();
const { resumeMode } = useLocation<{
resumeMode: ReaderResumeMode;
}>().state ?? { resumeMode: ReaderResumeMode.START };
const scrollElementRef = useRef<HTMLDivElement | null>(null);
const { backgroundColor } = ReaderService.useSettings();
const { chapterIndex: paramChapterIndex, mangaId: paramMangaId } = useParams<{
chapterIndex: string;
mangaId: string;
@@ -321,7 +354,7 @@ export const Reader = () => {
return null;
}
const isPlaceholderPageState = pages.length === 1 && pages[0].primary.url === requestManager.getBaseUrl();
const isPlaceholderPageState = totalPages === 1 && firstPageUrl === requestManager.getBaseUrl();
if (isPlaceholderPageState) {
return null;
}
@@ -350,3 +383,43 @@ export const Reader = () => {
</>
);
};
export const Reader = withPropsFrom(
memo(BaseReader),
[
useNavBarContext,
useReaderOverlayContext,
useReaderStateMangaContext,
useReaderStateChaptersContext,
useReaderStateSettingsContext,
ReaderService.useSettingsWithoutDefaultFlag,
userReaderStatePagesContext,
() => ({
firstPageUrl: userReaderStatePagesContext().pages[0].primary.url,
}),
],
[
'setTitle',
'setOverride',
'readerNavBarWidth',
'isVisible',
'setIsVisible',
'manga',
'setManga',
'shouldSkipDupChapters',
'setSettings',
'initialChapter',
'currentChapter',
'chapters',
'setReaderStateChapters',
'firstPageUrl',
'totalPages',
'setTotalPages',
'setCurrentPageIndex',
'setPageToScrollToIndex',
'setPages',
'setPageUrls',
'setPageLoadStates',
'setTransitionPageMode',
],
);

View File

@@ -412,9 +412,9 @@ export class ReaderControls {
});
};
clearTimeout(this.updateCurrentPageTimeout);
clearTimeout(ReaderControls.updateCurrentPageTimeout);
if (debounceChapterUpdate) {
this.updateCurrentPageTimeout = setTimeout(handleCurrentPageIndexChange, 1000);
ReaderControls.updateCurrentPageTimeout = setTimeout(handleCurrentPageIndexChange, 1000);
return;
}
@@ -480,7 +480,7 @@ export class ReaderControls {
break;
case TapZoneRegionType.PREVIOUS:
if (isContinuousReadingModeActive) {
this.scroll(
ReaderControls.scroll(
ScrollOffset.BACKWARD,
scrollDirection,
readingMode.value,
@@ -496,7 +496,7 @@ export class ReaderControls {
break;
case TapZoneRegionType.NEXT:
if (isContinuousReadingModeActive) {
this.scroll(
ReaderControls.scroll(
ScrollOffset.FORWARD,
scrollDirection,
readingMode.value,

View File

@@ -22,7 +22,10 @@ import {
ReadingMode,
} from '@/modules/reader/types/Reader.types.ts';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
import { updateReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
import {
convertFromReaderSettingsWithDefaultFlag,
updateReaderSettings,
} from '@/modules/reader/services/ReaderSettingsMetadata.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { getMetadataKey } from '@/modules/metadata/services/MetadataReader.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
@@ -64,11 +67,11 @@ export class ReaderService {
private static chapterUpdateQueues: Map<ChapterIdInfo['id'], Queue> = new Map();
private static getOrCreateChapterUpdateQueue(id: ChapterIdInfo['id']): Queue {
if (!this.chapterUpdateQueues.has(id)) {
this.chapterUpdateQueues.set(id, new Queue(1));
if (!ReaderService.chapterUpdateQueues.has(id)) {
ReaderService.chapterUpdateQueues.set(id, new Queue(1));
}
return this.chapterUpdateQueues.get(id)!;
return ReaderService.chapterUpdateQueues.get(id)!;
}
static useNavigateToChapter(chapter?: TChapterReader, resumeMode?: ReaderResumeMode): () => void {
@@ -95,7 +98,7 @@ export class ReaderService {
): void {
const key = `${currentChapter.id}_${nextChapter?.id}_${pageIndex}_${downloadAheadLimit}`;
this.downloadAheadQueue.enqueue(key, async () => {
ReaderService.downloadAheadQueue.enqueue(key, async () => {
const chapterIdsForDownloadAhead = getChapterIdsForDownloadAhead(
currentChapter,
nextChapter,
@@ -119,7 +122,7 @@ export class ReaderService {
static useUpdateChapter(): (patch: UpdateChapterPatchInput) => void {
const { manga } = useReaderStateMangaContext();
const { initialChapter, currentChapter, mangaChapters } = useReaderStateChaptersContext();
const { shouldSkipDupChapters } = this.useSettings();
const { shouldSkipDupChapters } = ReaderService.useSettings();
const {
settings: { deleteChaptersWhileReading, deleteChaptersWithBookmark, updateProgressAfterReading },
} = useMetadataServerSettings();
@@ -193,7 +196,9 @@ export class ReaderService {
.response.catch(defaultPromiseErrorHandler('ReaderService::useUpdateChapter'));
};
this.getOrCreateChapterUpdateQueue(currentChapter.id).enqueue(`${currentChapter.id}`, () => update());
ReaderService.getOrCreateChapterUpdateQueue(currentChapter.id).enqueue(`${currentChapter.id}`, () =>
update(),
);
},
[
manga?.id,
@@ -212,6 +217,10 @@ export class ReaderService {
return useReaderStateSettingsContext().settings;
}
static useSettingsWithoutDefaultFlag(): IReaderSettings {
return convertFromReaderSettingsWithDefaultFlag(ReaderService.useSettings());
}
static useGetThemeDirection(): Direction {
const { direction } = useTheme();
const { readingDirection } = ReaderService.useSettings();
@@ -323,11 +332,11 @@ export class ReaderService {
manga: MangaIdInfo,
profile?: ReadingMode,
): (
...args: OmitFirst<Parameters<typeof this.updateSetting<Setting>>>
) => ReturnType<typeof this.updateSetting<Setting>> {
...args: OmitFirst<Parameters<typeof ReaderService.updateSetting<Setting>>>
) => ReturnType<typeof ReaderService.updateSetting<Setting>> {
return useCallback(
(setting, value, commit, isGlobal) =>
this.updateSetting<Setting>(manga, setting, value, commit, isGlobal, profile),
ReaderService.updateSetting<Setting>(manga, setting, value, commit, isGlobal, profile),
[manga, profile],
);
}
@@ -336,17 +345,17 @@ export class ReaderService {
manga: MangaIdInfo,
profile?: ReadingMode,
): (
...args: OmitFirst<Parameters<typeof this.deleteSetting<Setting>>>
) => ReturnType<typeof this.deleteSetting<Setting>> {
...args: OmitFirst<Parameters<typeof ReaderService.deleteSetting<Setting>>>
) => ReturnType<typeof ReaderService.deleteSetting<Setting>> {
return useCallback(
(setting, isGlobal) => this.deleteSetting<Setting>(manga, setting, isGlobal, profile?.toString()),
(setting, isGlobal) => ReaderService.deleteSetting<Setting>(manga, setting, isGlobal, profile?.toString()),
[manga, profile],
);
}
static useOverlayMode(): { mode: ReaderOverlayMode; isDesktop: boolean; isMobile: boolean } {
const isTouchDevice = MediaQuery.useIsTouchDevice();
const { overlayMode } = this.useSettings();
const { overlayMode } = ReaderService.useSettings();
const isAutoModeSelected = overlayMode === ReaderOverlayMode.AUTO;
const isDesktopModeSelected = overlayMode === ReaderOverlayMode.DESKTOP;
@@ -364,7 +373,7 @@ export class ReaderService {
static useExit(): () => void {
const { manga } = useReaderStateMangaContext();
const { exitMode } = this.useSettings();
const { exitMode } = ReaderService.useSettings();
const handleBack = useBackButton();
const navigate = useNavigate();