Structure "reader" in sub-features
This commit is contained in:
266
src/features/reader/overlay/progress-bar/ReaderProgressBar.tsx
Normal file
266
src/features/reader/overlay/progress-bar/ReaderProgressBar.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
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';
|
||||
import {
|
||||
getNextIndexFromPage,
|
||||
getPage,
|
||||
getPageForMousePos,
|
||||
getProgressBarPositionInfo,
|
||||
} from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { getOptionForDirection as getOptionForDirectionImpl } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { ReaderProgressBarSlotsActionArea } from '@/features/reader/overlay/progress-bar/components/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/overlay/progress-bar/contexts/ReaderProgressBarContext.tsx';
|
||||
import { ReaderProgressBarSlotWrapper } from '@/features/reader/overlay/progress-bar/components/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/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,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 { BoxProps } from '@mui/material/Box';
|
||||
import { TooltipProps } from '@mui/material/Tooltip';
|
||||
import { IReaderSettings, ReaderTransitionPageMode } from '@/features/reader/Reader.types.ts';
|
||||
|
||||
interface SinglePageData {
|
||||
index: number;
|
||||
alt: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface PageData {
|
||||
name: string;
|
||||
primary: SinglePageData;
|
||||
secondary?: SinglePageData;
|
||||
}
|
||||
|
||||
export interface ReaderStatePages {
|
||||
totalPages: number;
|
||||
setTotalPages: React.Dispatch<React.SetStateAction<number>>;
|
||||
currentPageIndex: number;
|
||||
setCurrentPageIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||
pageToScrollToIndex: number | null;
|
||||
setPageToScrollToIndex: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
pageUrls: string[];
|
||||
setPageUrls: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
pageLoadStates: { url: string; loaded: boolean; error?: boolean }[];
|
||||
setPageLoadStates: React.Dispatch<React.SetStateAction<{ url: string; loaded: boolean; error?: boolean }[]>>;
|
||||
pages: PageData[];
|
||||
setPages: React.Dispatch<React.SetStateAction<PageData[]>>;
|
||||
transitionPageMode: ReaderTransitionPageMode;
|
||||
setTransitionPageMode: React.Dispatch<React.SetStateAction<ReaderTransitionPageMode>>;
|
||||
retryFailedPagesKeyPrefix: string;
|
||||
setRetryFailedPagesKeyPrefix: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export interface ReaderProgressBarProps
|
||||
extends Pick<ReaderStatePages, 'totalPages' | 'pages' | 'pageLoadStates' | 'currentPageIndex'>,
|
||||
Pick<IReaderSettings, 'progressBarPosition'> {}
|
||||
|
||||
export interface TReaderProgressCurrentPage extends PageData {
|
||||
pagesIndex: number;
|
||||
}
|
||||
|
||||
export interface ReaderProgressBarSlotProps extends Pick<IReaderSettings, 'progressBarPosition'> {
|
||||
pageName: string;
|
||||
slotProps?: {
|
||||
box?: BoxProps;
|
||||
tooltip?: Omit<TooltipProps, 'title' | 'children'>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CurrentPageSlotProps extends Pick<IReaderSettings, 'progressBarPosition'> {
|
||||
pageName: string;
|
||||
currentPagesIndex: number;
|
||||
pagesLength: number;
|
||||
isDragging: boolean;
|
||||
boxProps?: BoxProps;
|
||||
}
|
||||
|
||||
export type TReaderProgressBarContext = {
|
||||
isMaximized: boolean;
|
||||
setIsMaximized: (visible: boolean) => void;
|
||||
isDragging: boolean;
|
||||
setIsDragging: (isDragging: boolean) => void;
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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 {
|
||||
ReaderProgressBarProps,
|
||||
TReaderProgressCurrentPage,
|
||||
} from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { getOptionForDirection as getOptionForDirectionImpl } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { ProgressBarPosition } from '@/features/reader/Reader.types.ts';
|
||||
import { coerceIn } from '@/lib/HelperFunctions.ts';
|
||||
|
||||
export const getPage = (pageIndex: number, pages: ReaderProgressBarProps['pages']): TReaderProgressCurrentPage => {
|
||||
const pagesIndex = pages.findIndex(({ primary, secondary }) =>
|
||||
[primary.index, secondary?.index].includes(pageIndex),
|
||||
);
|
||||
const page = pages[coerceIn(pagesIndex, 0, pages.length - 1)];
|
||||
return {
|
||||
...page,
|
||||
pagesIndex,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* for the double page mode the secondary page index has to be used to be able to correctly detect if the last page is visible
|
||||
*
|
||||
*/
|
||||
export const getNextIndexFromPage = (page: ReaderProgressBarProps['pages'][number]) =>
|
||||
page.secondary?.index ?? page.primary.index;
|
||||
|
||||
export const getNextPageIndex = (
|
||||
offset: 'previous' | 'next',
|
||||
pagesIndex: number,
|
||||
pages: ReaderProgressBarProps['pages'],
|
||||
): number => {
|
||||
switch (offset) {
|
||||
case 'previous':
|
||||
return getNextIndexFromPage(pages[Math.max(0, pagesIndex - 1)]);
|
||||
case 'next':
|
||||
return getNextIndexFromPage(pages[Math.min(pages.length - 1, pagesIndex + 1)]);
|
||||
default:
|
||||
throw new Error(`Unexpected offset "${offset}"`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getPageForMousePos = (
|
||||
coordinates: { clientX: number; clientY: number },
|
||||
element: HTMLElement,
|
||||
pages: ReaderProgressBarProps['pages'],
|
||||
isHorizontalPosition: boolean,
|
||||
fullSegmentClicks: boolean,
|
||||
getOptionForDirection: typeof getOptionForDirectionImpl,
|
||||
): ReaderProgressBarProps['pages'][number] => {
|
||||
const pos = isHorizontalPosition ? coordinates.clientX : coordinates.clientY;
|
||||
|
||||
const { paddingTop, paddingBottom, paddingLeft, paddingRight } = getComputedStyle(element);
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const padding = isHorizontalPosition
|
||||
? parseFloat(paddingLeft) + parseFloat(paddingRight)
|
||||
: parseFloat(paddingTop) + parseFloat(paddingBottom);
|
||||
|
||||
const rectPos = isHorizontalPosition ? elementRect.left : elementRect.top;
|
||||
const rectSizeWithPadding = isHorizontalPosition ? elementRect.width : elementRect.height;
|
||||
const rectSize = rectSizeWithPadding - padding;
|
||||
|
||||
const mousePosRelativeToProgressBar = pos - rectPos - padding / 2;
|
||||
|
||||
const totalPages = pages.length - Number(!fullSegmentClicks);
|
||||
|
||||
const segmentWidth = rectSize / totalPages;
|
||||
const clickedSegmentIndex = Math.floor(mousePosRelativeToProgressBar / segmentWidth);
|
||||
const segmentMiddlePoint = (clickedSegmentIndex + (fullSegmentClicks ? 1 : 0.5)) * segmentWidth;
|
||||
const actualClickedSegmentIndex =
|
||||
mousePosRelativeToProgressBar <= segmentMiddlePoint ? clickedSegmentIndex : clickedSegmentIndex + 1;
|
||||
|
||||
const minPageIndex = Math.max(0, actualClickedSegmentIndex);
|
||||
const maxPageIndex = Math.min(minPageIndex, pages.length - 1);
|
||||
const newPageIndex = getOptionForDirection(maxPageIndex, pages.length - 1 - maxPageIndex);
|
||||
|
||||
return pages[newPageIndex];
|
||||
};
|
||||
|
||||
export const getProgressBarPositionInfo = (
|
||||
position: ProgressBarPosition,
|
||||
): {
|
||||
isBottom: boolean;
|
||||
isLeft: boolean;
|
||||
isRight: boolean;
|
||||
isHorizontal: boolean;
|
||||
isVertical: boolean;
|
||||
} => {
|
||||
const isBottom = position === ProgressBarPosition.BOTTOM;
|
||||
const isLeft = position === ProgressBarPosition.LEFT;
|
||||
const isRight = position === ProgressBarPosition.RIGHT;
|
||||
const isHorizontal = isBottom;
|
||||
const isVertical = isLeft || isRight;
|
||||
|
||||
return {
|
||||
isBottom,
|
||||
isLeft,
|
||||
isRight,
|
||||
isHorizontal,
|
||||
isVertical,
|
||||
};
|
||||
};
|
||||
@@ -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/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { getProgressBarPositionInfo } from '@/features/reader/overlay/progress-bar/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,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/Reader.types.ts';
|
||||
import { shouldForwardProp } from '@/features/core/utils/ShouldForwardProp.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { getProgressBarPositionInfo } from '@/features/reader/overlay/progress-bar/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/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { getProgressBarPositionInfo } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/features/reader/settings/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/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
|
||||
import { READER_PROGRESS_BAR_POSITION_TO_PLACEMENT } from '@/features/reader/settings/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/overlay/progress-bar/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,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/overlay/progress-bar/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/overlay/progress-bar/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>;
|
||||
};
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* 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/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/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { getProgressBarPositionInfo } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { ReaderProgressBarDirectionWrapper } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarDirectionWrapper.tsx';
|
||||
import {
|
||||
ReaderProgressBarProps,
|
||||
TReaderProgressBarContext,
|
||||
} from '@/features/reader/overlay/progress-bar/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/overlay/progress-bar/contexts/ReaderProgressBarContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { ReaderProgressBarSlotDesktop } from '@/features/reader/overlay/progress-bar/desktop/components/ReaderProgressBarSlotDesktop.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
import { getProgressBarPosition } from '@/features/reader/settings/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,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/overlay/progress-bar/components/ReaderProgressBarSlot.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/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,390 @@
|
||||
/*
|
||||
* 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/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/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ProgressBarPosition,
|
||||
ReaderStateChapters,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderProgressBarDirectionWrapper } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarDirectionWrapper.tsx';
|
||||
import { useReaderProgressBarContext } from '@/features/reader/overlay/progress-bar/contexts/ReaderProgressBarContext.tsx';
|
||||
import { useReaderOverlayContext } from '@/features/reader/overlay/contexts/ReaderOverlayContext.tsx';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { TReaderOverlayContext } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
import {
|
||||
ReaderProgressBarProps,
|
||||
TReaderProgressBarContext,
|
||||
} from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { ReaderProgressBarSlotMobile } from '@/features/reader/overlay/progress-bar/mobile/components/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/settings/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,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/Reader.types.ts';
|
||||
import { applyStyles } from '@/features/core/utils/ApplyStyles.ts';
|
||||
import { ReaderProgressBarSlot } from '@/features/reader/overlay/progress-bar/components/ReaderProgressBarSlot.tsx';
|
||||
|
||||
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,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/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/Reader.types.ts';
|
||||
import { SliderInput } from '@/features/core/components/inputs/SliderInput.tsx';
|
||||
import { AUTO_SCROLL_SPEED, DEFAULT_READER_SETTINGS } from '@/features/reader/settings/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/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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user