From f90e4597f883f8b32848d49f0ec592a5e4244ae8 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Thu, 2 Jan 2025 21:25:35 +0100 Subject: [PATCH] Make mobile progress bar position changeable --- .../components/overlay/ReaderOverlay.tsx | 18 +- .../overlay/ReaderOverlayHeaderMobile.tsx | 22 +- .../mobile/ReaderBottomBarMobile.tsx | 42 +- .../progress-bar/ReaderProgressBar.tsx | 5 +- .../ReaderProgressBarCurrentPageSlot.tsx | 2 +- .../ReaderProgressBarDirectionWrapper.tsx | 20 +- .../mobile/ReaderProgressBarSlotMobile.tsx | 38 +- .../variants/MobileReaderProgressBar.tsx | 376 +++++++++++++----- src/modules/reader/services/ReaderControls.ts | 2 +- .../reader/utils/ReaderProgressBar.utils.tsx | 16 +- 10 files changed, 371 insertions(+), 170 deletions(-) diff --git a/src/modules/reader/components/overlay/ReaderOverlay.tsx b/src/modules/reader/components/overlay/ReaderOverlay.tsx index 765a956e..ed4b0720 100644 --- a/src/modules/reader/components/overlay/ReaderOverlay.tsx +++ b/src/modules/reader/components/overlay/ReaderOverlay.tsx @@ -7,7 +7,7 @@ */ import Box from '@mui/material/Box'; -import { memo, useState } from 'react'; +import { memo, useCallback, useRef, useState } from 'react'; import { BaseReaderOverlayProps, MobileHeaderProps } from '@/modules/reader/types/ReaderOverlay.types.ts'; import { ReaderSettings } from '@/modules/reader/components/settings/ReaderSettings.tsx'; import { ReaderPageNumber } from '@/modules/reader/components/ReaderPageNumber.tsx'; @@ -17,6 +17,7 @@ import { ReaderOverlayHeaderMobile } from '@/modules/reader/components/overlay/R 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'; +import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; const BaseReaderOverlay = ({ isVisible, @@ -27,6 +28,13 @@ const BaseReaderOverlay = ({ Pick, 'isDesktop' | 'isMobile'>) => { const [areSettingsOpen, setAreSettingsOpen] = useState(false); + const [mobileHeaderHeight, setMobileHeaderHeight] = useState(0); + const mobileHeaderRef = useRef(null); + useResizeObserver( + mobileHeaderRef, + useCallback(() => setMobileHeaderHeight(mobileHeaderRef.current?.clientHeight ?? 0), [isMobile]), + ); + return ( {isDesktop && ( @@ -38,8 +46,12 @@ const BaseReaderOverlay = ({ {isMobile && ( <> - - setAreSettingsOpen(true)} isVisible={isVisible} /> + + setAreSettingsOpen(true)} + isVisible={isVisible} + topOffset={mobileHeaderHeight} + /> )} diff --git a/src/modules/reader/components/overlay/ReaderOverlayHeaderMobile.tsx b/src/modules/reader/components/overlay/ReaderOverlayHeaderMobile.tsx index defd65e1..8084483c 100644 --- a/src/modules/reader/components/overlay/ReaderOverlayHeaderMobile.tsx +++ b/src/modules/reader/components/overlay/ReaderOverlayHeaderMobile.tsx @@ -22,7 +22,7 @@ import { Link as RouterLink } from 'react-router-dom'; import { alpha } from '@mui/material/styles'; import Tooltip from '@mui/material/Tooltip'; import Slide from '@mui/material/Slide'; -import { memo } from 'react'; +import { forwardRef, memo } from 'react'; import { useGetOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts'; import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx'; import { actionToTranslationKey, ChapterAction, Chapters } from '@/modules/chapter/services/Chapters.ts'; @@ -44,15 +44,13 @@ import { useReaderScrollbarContext } from '@/modules/reader/contexts/ReaderScrol const DEFAULT_MANGA = { id: -1, title: '' }; const DEFAULT_CHAPTER = { id: -1, name: '', realUrl: '', isBookmarked: false }; -const BaseReaderOverlayHeaderMobile = ({ - isVisible, - manga, - currentChapter, - scrollbarYSize, -}: MobileHeaderProps & - Pick & - Pick & - Pick) => { +const BaseReaderOverlayHeaderMobile = forwardRef< + HTMLDivElement, + MobileHeaderProps & + Pick & + Pick & + Pick +>(({ isVisible, manga, currentChapter, scrollbarYSize }, ref) => { const { t } = useTranslation(); const getOptionForDirection = useGetOptionForDirection(); const handleBack = useBackButton(); @@ -66,7 +64,7 @@ const BaseReaderOverlayHeaderMobile = ({ : 'bookmark'; return ( - + ); -}; +}); export const ReaderOverlayHeaderMobile = withPropsFrom( memo(BaseReaderOverlayHeaderMobile), diff --git a/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx b/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx index 26b75975..0853095b 100644 --- a/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx +++ b/src/modules/reader/components/overlay/navigation/mobile/ReaderBottomBarMobile.tsx @@ -18,7 +18,7 @@ import DialogContent from '@mui/material/DialogContent'; import Tooltip from '@mui/material/Tooltip'; import { useTranslation } from 'react-i18next'; import Slide from '@mui/material/Slide'; -import { memo, useLayoutEffect } from 'react'; +import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react'; import { ReaderBottomBarMobileProps } from '@/modules/reader/types/ReaderOverlay.types.ts'; import { MobileReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx'; import { ReaderChapterList } from '@/modules/reader/components/overlay/navigation/ReaderChapterList.tsx'; @@ -27,6 +27,7 @@ import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/R 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'; +import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; const BaseReaderBottomBarMobile = ({ openSettings, @@ -35,38 +36,47 @@ const BaseReaderBottomBarMobile = ({ chapters, scrollbarXSize, scrollbarYSize, + topOffset = 0, }: ReaderBottomBarMobileProps & Pick & - Pick) => { + Pick & { topOffset?: number }) => { const { t } = useTranslation(); const chapterListPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-chapter-list-dialog' }); const quickSettingsPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-quick-settings-dialog' }); + const [bottomBarRefHeight, setBottomBarRefHeight] = useState(0); + const bottomBarRef = useRef(null); + useResizeObserver( + bottomBarRef, + useCallback(() => setBottomBarRefHeight(bottomBarRef.current?.clientHeight ?? 0), [bottomBarRefHeight]), + ); + useLayoutEffect(() => { chapterListPopupState.close(); }, [currentChapter?.id]); return ( <> - - - + + + alpha(theme.palette.background.paper, 0.95), pb: `max(${scrollbarXSize}px, env(safe-area-inset-bottom))`, boxShadow: 2, + pointerEvents: 'all', }} > - - + + {chapterListPopupState.isOpen && ( diff --git a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx index 35141496..e2144ef0 100644 --- a/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx +++ b/src/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx @@ -133,7 +133,7 @@ const BaseReaderProgressBar = ({ getNextIndexFromPage( getPageForMousePos( isTouchEvent ? e.touches[0] : e, - progressBarRef.current.getBoundingClientRect(), + progressBarRef.current, pages, isHorizontalPosition, fullSegmentClicks, @@ -175,13 +175,12 @@ const BaseReaderProgressBar = ({ setIsDragging(false)}> - + {pages.map((page, pagesIndex) => ( ; -}) => { +const BaseReaderProgressBarDirectionWrapper = forwardRef< + HTMLElement, + BoxProps & { + direction: ReturnType; + } +>(({ direction, ...boxProps }, ref) => { const [appTheme] = useLocalStorage('appTheme', 'default'); const [themeMode] = useLocalStorage('themeMode', ThemeMode.SYSTEM); const [pureBlackMode] = useLocalStorage('pureBlackMode', false); @@ -41,11 +41,11 @@ const BaseReaderProgressBarDirectionWrapper = ({ return ( -
{children}
+
); -}; +}); export const ReaderProgressBarDirectionWrapper = withPropsFrom( BaseReaderProgressBarDirectionWrapper, diff --git a/src/modules/reader/components/overlay/progress-bar/mobile/ReaderProgressBarSlotMobile.tsx b/src/modules/reader/components/overlay/progress-bar/mobile/ReaderProgressBarSlotMobile.tsx index 346dc717..057130c3 100644 --- a/src/modules/reader/components/overlay/progress-bar/mobile/ReaderProgressBarSlotMobile.tsx +++ b/src/modules/reader/components/overlay/progress-bar/mobile/ReaderProgressBarSlotMobile.tsx @@ -20,34 +20,34 @@ const SLOT_SX_PROP: NonNullable { - const isFirstPage = pagesIndex === 0; - const isLastPage = pagesIndex === totalPages - 1; - - const left = `${(pagesIndex / (totalPages - 1)) * 100}%`; + const position = `${(pagesIndex / (totalPages - 1)) * 100}%`; return ( ( - + {!isCurrentPage && ( + + )} ), ); diff --git a/src/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx b/src/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx index 41c06b75..d871bf47 100644 --- a/src/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx +++ b/src/modules/reader/components/overlay/progress-bar/variants/MobileReaderProgressBar.tsx @@ -12,13 +12,20 @@ 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 } from 'react'; +import { memo, useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import Slide, { SlideProps } from '@mui/material/Slide'; import { ReaderProgressBar } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBar.tsx'; import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx'; import { ReaderService } from '@/modules/reader/services/ReaderService.ts'; -import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; +import { getPage, getProgressBarPositionInfo } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx'; import { getOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts'; -import { ProgressBarPosition, ReaderResumeMode, ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts'; +import { + IReaderSettings, + ProgressBarPosition, + ProgressBarPositionAutoVertical, + 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'; @@ -28,57 +35,12 @@ import { ReaderProgressBarProps, TReaderProgressBarContext } from '@/modules/rea import { ReaderProgressBarSlotMobile } from '@/modules/reader/components/overlay/progress-bar/mobile/ReaderProgressBarSlotMobile.tsx'; import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx'; import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts'; +import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx'; -const PROGRESS_BAR_SLOT_PROPS: ComponentProps['slotProps'] = { - 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, - alignItems: 'stretch', - gap: 0, - }, - }, - progressBarSlotsActionArea: { - sx: { - height: '100%', - alignItems: 'center', - py: 2, - cursor: 'pointer', - }, - }, - progressBarSlotsContainer: { - sx: { - borderRadius: 100, - backgroundColor: 'background.default', - }, - }, - progressBarSlot: { - sx: { - height: '20px', - }, - }, - progressBarCurrentPageSlot: { - sx: { - display: 'flex', - alignItems: 'center', - zIndex: 1, - cursor: 'inherit', - }, - }, - progressBarPageTexts: { - base: { px: 1 }, - }, +const PROGRESS_BAR_POSITION_TO_SLIDE_DIRECTION: Record = { + [ProgressBarPosition.BOTTOM]: 'up', + [ProgressBarPosition.LEFT]: 'right', + [ProgressBarPosition.RIGHT]: 'left', }; const BaseMobileReaderProgressBar = ({ @@ -89,16 +51,37 @@ const BaseMobileReaderProgressBar = ({ isDragging, currentPageIndex, pages, - direction, + direction: readerDirection, + progressBarPosition, + progressBarPositionAutoVertical, + topOffset = 0, + bottomOffset = 0, }: Pick & Pick & Pick & - Pick & { + Pick & + Pick & { direction: ReturnType; + topOffset?: number; + bottomOffset?: number; }) => { const openNextChapter = ReaderService.useNavigateToChapter(nextChapter, ReaderResumeMode.START); const openPreviousChapter = ReaderService.useNavigateToChapter(previousChapter, ReaderResumeMode.END); + const [, setRefreshProgressBarPosition] = useState({}); + useResizeObserver( + window.document.documentElement, + useCallback(() => setRefreshProgressBarPosition({}), []), + ); + + const finalProgressBarPosition = + window.innerHeight - topOffset - bottomOffset > window.innerWidth && + progressBarPositionAutoVertical !== ProgressBarPositionAutoVertical.OFF + ? (progressBarPositionAutoVertical as unknown as ProgressBarPosition) + : progressBarPosition; + + const { isLeft, isRight, isVertical, isHorizontal } = getProgressBarPositionInfo(finalProgressBarPosition); + const finalReaderDirection = isHorizontal ? readerDirection : 'ltr'; const currentPagesIndex = useMemo(() => getPage(currentPageIndex, pages).pagesIndex, [currentPageIndex, pages]); const progressBarCurrentPage = useMemo( @@ -107,12 +90,22 @@ const BaseMobileReaderProgressBar = ({ ), }), - [currentPagesIndex, pages.length, isDragging], + [currentPagesIndex, pages.length, isDragging, isVertical, isHorizontal], ); useLayoutEffect(() => { @@ -130,58 +123,222 @@ const BaseMobileReaderProgressBar = ({ }, [isVisible]); return ( - - + - alpha(theme.palette.background.paper, 0.85), boxShadow: 2 }} - > - {getOptionForDirection(, , direction)} - - ( - - ), - [], - )} - slotProps={{ - ...PROGRESS_BAR_SLOT_PROPS, - progressBarReadPages: { - sx: { - height: '20px', - backgroundColor: 'primary.main', - borderRadius: '400px 0 0 400px', - width: `${(Math.max(0, getPage(currentPageIndex, pages).pagesIndex) / (pages.length - 1)) * 100}%`, - }, - }, + - alpha(theme.palette.background.paper, 0.85), boxShadow: 2 }} > - {getOptionForDirection(, , direction)} - - - + alpha(theme.palette.background.paper, 0.85), + boxShadow: 2, + }} + > + {getOptionForDirection( + , + , + finalReaderDirection, + )} + + ( + + ), + [isVertical, isHorizontal], + )} + slotProps={{ + 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, + }), + }, + }, + }, + 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} + /> + alpha(theme.palette.background.paper, 0.85), boxShadow: 2 }} + > + {getOptionForDirection( + , + , + finalReaderDirection, + )} + + + + ); }; @@ -193,6 +350,7 @@ export const MobileReaderProgressBar = withPropsFrom( useReaderProgressBarContext, userReaderStatePagesContext, () => ({ direction: ReaderService.useGetThemeDirection() }), + ReaderService.useSettingsWithoutDefaultFlag, ], [ 'previousChapter', @@ -203,5 +361,7 @@ export const MobileReaderProgressBar = withPropsFrom( 'currentPageIndex', 'pages', 'direction', + 'progressBarPosition', + 'progressBarPositionAutoVertical', ], ); diff --git a/src/modules/reader/services/ReaderControls.ts b/src/modules/reader/services/ReaderControls.ts index 590d78b3..b69da746 100644 --- a/src/modules/reader/services/ReaderControls.ts +++ b/src/modules/reader/services/ReaderControls.ts @@ -614,7 +614,7 @@ export class ReaderControls { const newPageIndex = getNextIndexFromPage( getPageForMousePos( coordinates, - progressBarRef.current.getBoundingClientRect(), + progressBarRef.current, pages, isHorizontal, fullSegmentClicks, diff --git a/src/modules/reader/utils/ReaderProgressBar.utils.tsx b/src/modules/reader/utils/ReaderProgressBar.utils.tsx index 9e568852..db01c65e 100644 --- a/src/modules/reader/utils/ReaderProgressBar.utils.tsx +++ b/src/modules/reader/utils/ReaderProgressBar.utils.tsx @@ -45,17 +45,25 @@ export const getNextPageIndex = ( export const getPageForMousePos = ( coordinates: { clientX: number; clientY: number }, - elementRect: DOMRect, + element: HTMLElement, pages: ReaderProgressBarProps['pages'], isHorizontalPosition: boolean, fullSegmentClicks: boolean, getOptionForDirection: typeof getOptionForDirectionImpl, ): ReaderProgressBarProps['pages'][number] => { const pos = isHorizontalPosition ? coordinates.clientX : coordinates.clientY; - const rectPos = isHorizontalPosition ? elementRect.left : elementRect.top; - const rectSize = isHorizontalPosition ? elementRect.width : elementRect.height; - const mousePosRelativeToProgressBar = pos - rectPos; + 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);