Files
suwayomi-material-you-webui/src/features/reader/overlay/progress-bar/ReaderProgressBar.tsx

252 lines
12 KiB
TypeScript
Raw Normal View History

2024-09-28 03:24:16 +02:00
/*
* 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';
2024-09-28 03:24:16 +02:00
import ClickAwayListener from '@mui/material/ClickAwayListener';
import { TypographyProps } from '@mui/material/Typography';
import { StackProps } from '@mui/material/Stack';
import { ReaderProgressBarProps } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
2025-08-16 02:02:13 +02:00
import { ReaderProgressBarPageNumber } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarPageNumber.tsx';
import { ReaderProgressBarContainer } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarContainer.tsx';
import { ReaderProgressBarRoot } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarRoot.tsx';
import { ReaderProgressBarSlotsContainer } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarSlotsContainer.tsx';
import { ProgressBarHighlightReadPages } from '@/features/reader/overlay/progress-bar/components/ProgressBarHighlightReadPages.tsx';
import { ReaderProgressBarCurrentPageSlot } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarCurrentPageSlot.tsx';
2024-09-28 03:24:16 +02:00
import {
getNextIndexFromPage,
2024-09-28 03:24:16 +02:00
getPage,
getPageForMousePos,
getProgressBarPositionInfo,
2025-08-16 02:02:13 +02:00
} from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
2025-08-15 22:02:58 +02:00
import { getOptionForDirection as getOptionForDirectionImpl } from '@/features/theme/services/ThemeCreator.ts';
2025-08-16 02:02:13 +02:00
import { ReaderProgressBarSlotsActionArea } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarSlotsActionArea.tsx';
2025-08-15 22:02:58 +02:00
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
import { withPropsFrom } from '@/base/hoc/withPropsFrom.tsx';
2025-08-16 02:02:13 +02:00
import { ReaderProgressBarSlotWrapper } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarSlotWrapper.tsx';
import { useResizeObserver } from '@/base/hooks/useResizeObserver.tsx';
import { ReadingMode } from '@/features/reader/Reader.types.ts';
import { useReaderStore, useReaderStoreShallow } from '@/features/reader/ReaderStore.ts';
2024-09-28 03:24:16 +02:00
2024-12-21 03:46:55 +01:00
const BaseReaderProgressBar = ({
2024-09-28 03:24:16 +02:00
slotProps,
slots,
createProgressBarSlot,
progressBarPosition,
2024-12-21 03:46:55 +01:00
openPage,
direction,
fullSegmentClicks,
2024-12-21 03:46:55 +01:00
}: ReaderProgressBarProps &
Pick<ComponentProps<typeof ReaderProgressBarSlotWrapper>, 'createProgressBarSlot'> & {
2024-12-21 03:46:55 +01:00
slotProps?: {
container?: StackProps;
progressBarRoot?: StackProps;
progressBarSlotsActionArea?: StackProps;
progressBarSlotsContainer?: StackProps;
progressBarSlot?: BoxProps;
progressBarReadPages?: BoxProps;
progressBarCurrentPageSlot?: BoxProps;
progressBarPageTexts?: {
base?: TypographyProps;
current?: TypographyProps;
total?: TypographyProps;
};
2024-09-28 03:24:16 +02:00
};
2024-12-21 03:46:55 +01:00
slots?: {
progressBarCurrentPage?: ReactNode;
};
openPage: ReturnType<typeof ReaderControls.useOpenPage>;
direction: ReturnType<typeof ReaderService.useGetThemeDirection>;
fullSegmentClicks: boolean;
2024-12-21 03:46:55 +01:00
}) => {
const { pages, pageLoadStates, totalPages, currentPageIndex } = useReaderStoreShallow((state) => ({
pages: state.pages.pages,
pageLoadStates: state.pages.pageLoadStates,
totalPages: state.pages.totalPages,
currentPageIndex: state.pages.currentPageIndex,
}));
const readingMode = useReaderStore((state) => state.settings.readingMode.value);
const { isDragging, setIsDragging } = useReaderStoreShallow((state) => ({
isDragging: state.progressBar.isDragging,
setIsDragging: state.progressBar.setIsDragging,
}));
2024-09-28 03:24:16 +02:00
const progressBarRef = useRef<HTMLDivElement | null>(null);
2025-04-18 20:42:41 +02:00
const draggingDetectionTimeout = useRef<NodeJS.Timeout>(undefined);
2024-09-28 03:24:16 +02:00
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]);
2024-09-28 03:24:16 +02:00
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(
2024-12-03 18:00:46 +01:00
openPage,
2024-09-28 03:24:16 +02:00
progressBarRef,
isDragging,
currentPage,
pages,
progressBarPosition,
getOptionForDirection,
fullSegmentClicks,
2024-09-28 03:24:16 +02:00
);
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);
};
2024-09-28 03:24:16 +02:00
return (
<ReaderProgressBarContainer {...slotProps?.container} progressBarPosition={progressBarPosition}>
<ReaderProgressBarRoot {...slotProps?.progressBarRoot}>
<ReaderProgressBarPageNumber
{...slotProps?.progressBarPageTexts?.base}
{...slotProps?.progressBarPageTexts?.current}
sx={[
{ width: `${totalPagesTextWidth}px` },
2024-09-28 03:24:16 +02:00
...(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)}
2024-09-28 03:24:16 +02:00
>
{currentPage.name}
</ReaderProgressBarPageNumber>
<ClickAwayListener onClickAway={() => setIsDragging(false)}>
<ReaderProgressBarSlotsActionArea
{...slotProps?.progressBarSlotsActionArea}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onTouchStart={handleMouseDown}
onTouchEnd={handleMouseUp}
2024-09-28 03:24:16 +02:00
>
<ReaderProgressBarSlotsContainer ref={progressBarRef} {...slotProps?.progressBarSlotsContainer}>
2024-09-28 03:24:16 +02:00
{pages.map((page, pagesIndex) => (
<ReaderProgressBarSlotWrapper
{...slotProps?.progressBarSlot}
2024-09-28 03:24:16 +02:00
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}
/>
2024-09-28 03:24:16 +02:00
))}
<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}
2024-09-28 03:24:16 +02:00
{...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)}
2024-09-28 03:24:16 +02:00
>
{totalPages}
</ReaderProgressBarPageNumber>
</ReaderProgressBarRoot>
</ReaderProgressBarContainer>
);
};
2024-12-21 03:46:55 +01:00
export const ReaderProgressBar = withPropsFrom(
2024-12-22 00:41:56 +01:00
memo(BaseReaderProgressBar),
[() => ({ openPage: ReaderControls.useOpenPage() })],
['openPage'],
2024-12-21 03:46:55 +01:00
);