Structure "reader" in sub-features
This commit is contained in:
69
src/features/reader/overlay/ReaderOverlay.tsx
Normal file
69
src/features/reader/overlay/ReaderOverlay.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { BaseReaderOverlayProps, MobileHeaderProps } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
import { ReaderSettings } from '@/features/reader/settings/screens/ReaderSettings.tsx';
|
||||
import { ReaderPageNumber } from '@/features/reader/overlay/components/ReaderPageNumber.tsx';
|
||||
import { StandardReaderProgressBar } from '@/features/reader/overlay/progress-bar/desktop/StandardReaderProgressBar.tsx';
|
||||
import { ReaderNavBarDesktop } from '@/features/reader/overlay/navigation/desktop/ReaderNavBarDesktop.tsx';
|
||||
import { ReaderOverlayHeaderMobile } from '@/features/reader/overlay/mobile/ReaderOverlayHeaderMobile.tsx';
|
||||
import { ReaderBottomBarMobile } from '@/features/reader/overlay/navigation/mobile/ReaderBottomBarMobile.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
|
||||
const BaseReaderOverlay = ({
|
||||
isVisible,
|
||||
isDesktop,
|
||||
isMobile,
|
||||
}: BaseReaderOverlayProps &
|
||||
MobileHeaderProps &
|
||||
Pick<ReturnType<typeof ReaderService.useOverlayMode>, 'isDesktop' | 'isMobile'>) => {
|
||||
const [areSettingsOpen, setAreSettingsOpen] = useState(false);
|
||||
|
||||
const [mobileHeaderHeight, setMobileHeaderHeight] = useState(0);
|
||||
const mobileHeaderRef = useRef<HTMLDivElement>(null);
|
||||
useResizeObserver(
|
||||
mobileHeaderRef,
|
||||
useCallback(() => setMobileHeaderHeight(mobileHeaderRef.current?.clientHeight ?? 0), [isMobile]),
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'absolute', width: '100%', height: '100%', pointerEvents: 'none', zIndex: 1 }}>
|
||||
{isDesktop && (
|
||||
<>
|
||||
<StandardReaderProgressBar />
|
||||
<ReaderNavBarDesktop isVisible={isVisible} openSettings={() => setAreSettingsOpen(true)} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMobile && (
|
||||
<>
|
||||
<ReaderOverlayHeaderMobile ref={mobileHeaderRef} isVisible={isVisible} />
|
||||
<ReaderBottomBarMobile
|
||||
openSettings={() => setAreSettingsOpen(true)}
|
||||
isVisible={isVisible}
|
||||
topOffset={mobileHeaderHeight}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ReaderSettings isOpen={areSettingsOpen} close={() => setAreSettingsOpen(false)} />
|
||||
|
||||
<ReaderPageNumber />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderOverlay = withPropsFrom(
|
||||
memo(BaseReaderOverlay),
|
||||
[ReaderService.useOverlayMode],
|
||||
['isDesktop', 'isMobile'],
|
||||
);
|
||||
26
src/features/reader/overlay/ReaderOverlay.types.ts
Normal file
26
src/features/reader/overlay/ReaderOverlay.types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
export interface BaseReaderOverlayProps {
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
export interface MobileHeaderProps extends BaseReaderOverlayProps {}
|
||||
|
||||
interface ReaderNavBarBaseProps extends BaseReaderOverlayProps {
|
||||
openSettings: () => void;
|
||||
}
|
||||
|
||||
export interface ReaderBottomBarMobileProps extends ReaderNavBarBaseProps {}
|
||||
|
||||
export interface ReaderNavBarDesktopProps extends ReaderNavBarBaseProps {}
|
||||
|
||||
export type TReaderOverlayContext = {
|
||||
isVisible: boolean;
|
||||
setIsVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
120
src/features/reader/overlay/components/ReaderPageNumber.tsx
Normal file
120
src/features/reader/overlay/components/ReaderPageNumber.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Box from '@mui/material/Box';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import {
|
||||
IReaderSettings,
|
||||
ProgressBarType,
|
||||
ReadingDirection,
|
||||
TReaderScrollbarContext,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.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 { reverseString } from '@/util/Strings.ts';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import {
|
||||
ReaderStatePages,
|
||||
TReaderProgressBarContext,
|
||||
} from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
|
||||
const BaseReaderPageNumber = ({
|
||||
isDesktop,
|
||||
scrollbarXSize,
|
||||
readerNavBarWidth,
|
||||
isMaximized,
|
||||
currentPageIndex,
|
||||
pages,
|
||||
totalPages,
|
||||
progressBarType,
|
||||
shouldShowPageNumber,
|
||||
readingDirection,
|
||||
}: Pick<TReaderScrollbarContext, 'scrollbarXSize'> &
|
||||
Pick<ReturnType<typeof ReaderService.useOverlayMode>, 'isDesktop'> &
|
||||
Pick<NavbarContextType, 'readerNavBarWidth'> &
|
||||
Pick<TReaderProgressBarContext, 'isMaximized'> &
|
||||
Pick<ReaderStatePages, 'currentPageIndex' | 'pages' | 'totalPages'> &
|
||||
Pick<IReaderSettings, 'progressBarType' | 'shouldShowPageNumber' | 'readingDirection'>) => {
|
||||
const pageName = useMemo(() => {
|
||||
const currentPageName = getPage(currentPageIndex, pages).name;
|
||||
const SEPARATOR = '/';
|
||||
const tmpPageName = `${currentPageName}${SEPARATOR}${totalPages}`;
|
||||
|
||||
return readingDirection === ReadingDirection.LTR ? tmpPageName : reverseString(tmpPageName, SEPARATOR);
|
||||
}, [currentPageIndex, pages, totalPages, readingDirection]);
|
||||
|
||||
if (!shouldShowPageNumber) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMaximized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDesktop && progressBarType === ProgressBarType.STANDARD) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isMaximized && progressBarType === ProgressBarType.HIDDEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
left: readerNavBarWidth,
|
||||
right: 0,
|
||||
bottom: (theme) => `max(calc(${theme.spacing(1)} + ${scrollbarXSize}px), env(safe-area-inset-bottom))`,
|
||||
alignItems: 'center',
|
||||
transition: (theme) => `left 0.${theme.transitions.duration.shortest}s`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
p: 0.5,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: 'white' }}>{pageName}</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderPageNumber = withPropsFrom(
|
||||
BaseReaderPageNumber,
|
||||
[
|
||||
ReaderService.useOverlayMode,
|
||||
useReaderScrollbarContext,
|
||||
useNavBarContext,
|
||||
useReaderProgressBarContext,
|
||||
userReaderStatePagesContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
],
|
||||
[
|
||||
'isDesktop',
|
||||
'scrollbarXSize',
|
||||
'readerNavBarWidth',
|
||||
'isMaximized',
|
||||
'currentPageIndex',
|
||||
'pages',
|
||||
'totalPages',
|
||||
'progressBarType',
|
||||
'shouldShowPageNumber',
|
||||
'readingDirection',
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
import { TReaderOverlayContext } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
|
||||
export const ReaderOverlayContext = createContext<TReaderOverlayContext>({
|
||||
isVisible: false,
|
||||
setIsVisible: () => {},
|
||||
});
|
||||
|
||||
export const useReaderOverlayContext = () => useContext(ReaderOverlayContext);
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { ReactNode, useMemo, useState } from 'react';
|
||||
import { ReaderOverlayContext } from '@/features/reader/overlay/contexts/ReaderOverlayContext.tsx';
|
||||
|
||||
export const ReaderOverlayContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const value = useMemo(() => ({ isVisible, setIsVisible }), [isVisible]);
|
||||
|
||||
return <ReaderOverlayContext.Provider value={value}>{children}</ReaderOverlayContext.Provider>;
|
||||
};
|
||||
@@ -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 { MutableRefObject, useEffect } from 'react';
|
||||
import { TReaderOverlayContext } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
import { TReaderTapZoneContext } from '@/features/reader/tap-zones/TapZoneLayout.types.ts';
|
||||
|
||||
export const useReaderHideOverlayOnUserScroll = (
|
||||
isOverlayVisible: boolean,
|
||||
setIsOverlayVisible: TReaderOverlayContext['setIsVisible'],
|
||||
showPreview: TReaderTapZoneContext['showPreview'],
|
||||
setShowPreview: TReaderTapZoneContext['setShowPreview'],
|
||||
scrollElementRef: MutableRefObject<HTMLDivElement | null>,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (isOverlayVisible) {
|
||||
setIsOverlayVisible(false);
|
||||
}
|
||||
|
||||
if (showPreview) {
|
||||
setShowPreview(false);
|
||||
}
|
||||
};
|
||||
|
||||
scrollElementRef.current?.addEventListener('wheel', handleScroll);
|
||||
scrollElementRef.current?.addEventListener('touchmove', handleScroll);
|
||||
return () => {
|
||||
scrollElementRef.current?.removeEventListener('wheel', handleScroll);
|
||||
scrollElementRef.current?.removeEventListener('touchmove', handleScroll);
|
||||
};
|
||||
}, [isOverlayVisible, showPreview]);
|
||||
};
|
||||
141
src/features/reader/overlay/mobile/ReaderOverlayHeaderMobile.tsx
Normal file
141
src/features/reader/overlay/mobile/ReaderOverlayHeaderMobile.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { bindMenu, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import Link from '@mui/material/Link';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { forwardRef, memo } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { TypographyMaxLines } from '@/features/core/components/texts/TypographyMaxLines.tsx';
|
||||
import { makeToast } from '@/features/core/utils/Toast.ts';
|
||||
import { MobileHeaderProps } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { AppRoutes } from '@/features/core/AppRoute.constants.ts';
|
||||
import {
|
||||
ReaderStateChapters,
|
||||
TReaderScrollbarContext,
|
||||
TReaderStateMangaContext,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { ReaderLibraryButton } from '@/features/reader/overlay/navigation/components/ReaderLibraryButton.tsx';
|
||||
import { ReaderBookmarkButton } from '@/features/reader/overlay/navigation/components/ReaderBookmarkButton.tsx';
|
||||
import { FALLBACK_CHAPTER } from '@/features/chapter/Chapter.constants.ts';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { ReaderExitButton } from '@/features/reader/overlay/navigation/components/ReaderExitButton.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
|
||||
const DEFAULT_MANGA = { ...FALLBACK_MANGA, title: '' };
|
||||
|
||||
const BaseReaderOverlayHeaderMobile = forwardRef<
|
||||
HTMLDivElement,
|
||||
MobileHeaderProps &
|
||||
Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<ReaderStateChapters, 'currentChapter'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarYSize'>
|
||||
>(({ isVisible, manga, currentChapter, scrollbarYSize }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const popupState = usePopupState({ popupId: 'reader-overlay-more-menu', variant: 'popover' });
|
||||
|
||||
const { id: mangaId, title } = manga ?? DEFAULT_MANGA;
|
||||
const { id: chapterId, name, realUrl, isBookmarked } = currentChapter ?? FALLBACK_CHAPTER;
|
||||
|
||||
return (
|
||||
<Slide direction="down" in={isVisible} ref={ref}>
|
||||
<Stack
|
||||
sx={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: `${scrollbarYSize}px`,
|
||||
p: 2,
|
||||
pt: (theme) => `max(env(safe-area-inset-top), ${theme.spacing(2)})`,
|
||||
backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.95),
|
||||
pointerEvents: 'all',
|
||||
boxShadow: 2,
|
||||
}}
|
||||
>
|
||||
<ReaderExitButton />
|
||||
<Stack sx={{ flexGrow: 1 }}>
|
||||
{manga && currentChapter ? (
|
||||
<>
|
||||
<CustomTooltip title={title}>
|
||||
<TypographyMaxLines lines={1} component="h1" variant="h5">
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={AppRoutes.manga.path(mangaId)}
|
||||
sx={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={name}>
|
||||
<TypographyMaxLines lines={1}>{name}</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
</>
|
||||
) : (
|
||||
<LoadingPlaceholder />
|
||||
)}
|
||||
</Stack>
|
||||
<ReaderLibraryButton />
|
||||
<ReaderBookmarkButton id={chapterId} isBookmarked={isBookmarked} />
|
||||
<IconButton {...bindTrigger(popupState)} color="inherit">
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
disabled={!realUrl}
|
||||
href={realUrl ?? ''}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('global.button.open_browser')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
disabled={!realUrl}
|
||||
href={realUrl ? requestManager.getWebviewUrl(realUrl) : ''}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{t('global.button.open_webview')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!realUrl}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(title);
|
||||
makeToast(t('global.label.copied_clipboard'), 'info');
|
||||
}}
|
||||
>
|
||||
{t('global.label.share')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
</Slide>
|
||||
);
|
||||
});
|
||||
|
||||
export const ReaderOverlayHeaderMobile = withPropsFrom(
|
||||
memo(BaseReaderOverlayHeaderMobile),
|
||||
[useReaderStateMangaContext, useReaderStateChaptersContext, useReaderScrollbarContext],
|
||||
['manga', 'currentChapter', 'scrollbarYSize'],
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { memo } from 'react';
|
||||
import BookmarkIcon from '@mui/icons-material/Bookmark';
|
||||
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { ChapterAction, TChapterReader } from '@/features/chapter/Chapter.types.ts';
|
||||
import { CHAPTER_ACTION_TO_TRANSLATION } from '@/features/chapter/Chapter.constants.ts';
|
||||
|
||||
const BaseReaderBookmarkButton = ({ id, isBookmarked }: Pick<TChapterReader, 'id' | 'isBookmarked'>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const bookmarkAction: Extract<ChapterAction, 'unbookmark' | 'bookmark'> = isBookmarked ? 'unbookmark' : 'bookmark';
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION[bookmarkAction].action.single)}>
|
||||
<IconButton onClick={() => Chapters.performAction(bookmarkAction, [id], {})} color="inherit">
|
||||
{isBookmarked ? <BookmarkIcon /> : <BookmarkBorderIcon />}
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderBookmarkButton = memo(BaseReaderBookmarkButton);
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Virtuoso, VirtuosoProps } from 'react-virtuoso';
|
||||
import { useMemo } from 'react';
|
||||
import { ReaderStateChapters } from '@/features/reader/Reader.types.ts';
|
||||
import { ChapterListCard } from '@/features/chapter/components/cards/ChapterListCard.tsx';
|
||||
|
||||
const onSelectNoop = () => {};
|
||||
|
||||
export const ReaderChapterList = ({
|
||||
currentChapter,
|
||||
chapters,
|
||||
style,
|
||||
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter'> & Pick<VirtuosoProps<any, any>, 'style'>) => {
|
||||
const currentChapterIndex = useMemo(
|
||||
() => currentChapter && chapters.findIndex((chapter) => chapter.id === currentChapter.id),
|
||||
[currentChapter, chapters],
|
||||
);
|
||||
|
||||
return (
|
||||
<Virtuoso
|
||||
style={{
|
||||
height: `calc(${chapters.length} * 100px)`,
|
||||
...style,
|
||||
}}
|
||||
initialTopMostItemIndex={currentChapterIndex ?? 0}
|
||||
totalCount={chapters.length}
|
||||
computeItemKey={(index) => chapters[index].id}
|
||||
itemContent={(index) => (
|
||||
<ChapterListCard
|
||||
index={index}
|
||||
chapters={chapters}
|
||||
isSortDesc
|
||||
mode="reader"
|
||||
showChapterNumber={false}
|
||||
selected={null}
|
||||
onSelect={onSelectNoop}
|
||||
selectable={false}
|
||||
isActiveChapter={index === currentChapterIndex}
|
||||
/>
|
||||
)}
|
||||
increaseViewportBy={400}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ArrowBack from '@mui/icons-material/ArrowBack';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import { memo } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
|
||||
const BaseReaderExitButton = ({ exit }: { exit: ReturnType<typeof ReaderService.useExit> }) => {
|
||||
const { t } = useTranslation();
|
||||
const getOptionForDirection = useGetOptionForDirection();
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t('reader.button.exit')}>
|
||||
<IconButton sx={{ marginRight: 2 }} onClick={exit} color="inherit">
|
||||
{getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)}
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderExitButton = withPropsFrom(
|
||||
memo(BaseReaderExitButton),
|
||||
[() => ({ exit: ReaderService.useExit() })],
|
||||
['exit'],
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||
import { memo } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { useManageMangaLibraryState } from '@/features/manga/hooks/useManageMangaLibraryState.tsx';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { TReaderStateMangaContext } from '@/features/reader/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
|
||||
const ACTION_FALLBACK_MANGA = {
|
||||
...FALLBACK_MANGA,
|
||||
title: 'Fallback',
|
||||
inLibrary: false,
|
||||
};
|
||||
|
||||
const BaseReaderLibraryButton = ({ manga }: Pick<TReaderStateMangaContext, 'manga'>) => {
|
||||
const { inLibrary } = manga ?? ACTION_FALLBACK_MANGA;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { CategorySelectComponent, updateLibraryState } = useManageMangaLibraryState(
|
||||
manga ?? ACTION_FALLBACK_MANGA,
|
||||
true,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomTooltip
|
||||
title={inLibrary ? t('manga.action.library.remove.label.action') : t('manga.button.add_to_library')}
|
||||
>
|
||||
<IconButton onClick={updateLibraryState} color="inherit">
|
||||
{inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
|
||||
{CategorySelectComponent}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderLibraryButton = withPropsFrom(
|
||||
memo(BaseReaderLibraryButton),
|
||||
[useReaderStateMangaContext],
|
||||
['manga'],
|
||||
);
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { ReaderNavBarDesktopProps } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
import { ReaderNavContainer } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavContainer.tsx';
|
||||
import { ReaderNavBarDesktopMetadata } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopMetadata.tsx';
|
||||
import { ReaderNavBarDesktopPageNavigation } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopPageNavigation.tsx';
|
||||
import { ReaderNavBarDesktopChapterNavigation } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopChapterNavigation.tsx';
|
||||
import { ReaderNavBarDesktopQuickSettings } from '@/features/reader/overlay/navigation/desktop/quick-settings/ReaderNavBarDesktopQuickSettings.tsx';
|
||||
import { ReaderNavBarDesktopActions } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopActions.tsx';
|
||||
import { useNavBarContext } from '@/features/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { LoadingPlaceholder } from '@/features/core/components/feedback/LoadingPlaceholder.tsx';
|
||||
import { NavbarContextType } from '@/features/navigation-bar/NavigationBar.types.ts';
|
||||
import { IReaderSettings, ReaderStateChapters, TReaderStateMangaContext } from '@/features/reader/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { ReaderExitButton } from '@/features/reader/overlay/navigation/components/ReaderExitButton.tsx';
|
||||
|
||||
const useGetPreviousNavBarStaticValue = (isVisible: boolean, isStaticNav: boolean) => {
|
||||
const wasNavBarStaticRef = useRef(isStaticNav);
|
||||
const wasNavBarStaticPreviousRef = useRef(isStaticNav);
|
||||
|
||||
const resetWasNavBarStaticValue = wasNavBarStaticPreviousRef.current !== wasNavBarStaticRef.current && !isVisible;
|
||||
if (resetWasNavBarStaticValue) {
|
||||
wasNavBarStaticRef.current = false;
|
||||
}
|
||||
|
||||
const didNavBarStaticValueChange = wasNavBarStaticPreviousRef.current !== isStaticNav;
|
||||
if (didNavBarStaticValueChange) {
|
||||
wasNavBarStaticRef.current = wasNavBarStaticPreviousRef.current;
|
||||
wasNavBarStaticPreviousRef.current = isStaticNav;
|
||||
}
|
||||
|
||||
return wasNavBarStaticRef.current;
|
||||
};
|
||||
|
||||
const BaseReaderNavBarDesktop = ({
|
||||
isVisible,
|
||||
openSettings,
|
||||
setReaderNavBarWidth,
|
||||
manga,
|
||||
chapters,
|
||||
currentChapter,
|
||||
previousChapter,
|
||||
nextChapter,
|
||||
isStaticNav,
|
||||
}: ReaderNavBarDesktopProps &
|
||||
Pick<NavbarContextType, 'setReaderNavBarWidth'> &
|
||||
Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<ReaderStateChapters, 'currentChapter' | 'previousChapter' | 'nextChapter' | 'chapters'> &
|
||||
Pick<IReaderSettings, 'isStaticNav'>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateReaderSettings = ReaderService.useCreateUpdateSetting(manga ?? FALLBACK_MANGA);
|
||||
|
||||
const [navBarElement, setNavBarElement] = useState<HTMLDivElement | null>();
|
||||
useResizeObserver(
|
||||
navBarElement,
|
||||
useCallback(() => {
|
||||
if (!isStaticNav) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReaderNavBarWidth(navBarElement!.offsetWidth);
|
||||
}, [navBarElement, isStaticNav]),
|
||||
);
|
||||
useLayoutEffect(() => () => setReaderNavBarWidth(0), []);
|
||||
|
||||
const wasNavBarStatic = useGetPreviousNavBarStaticValue(isVisible, isStaticNav);
|
||||
const changedNavBarStaticValue = wasNavBarStatic && isVisible;
|
||||
const drawerTransitionDuration = changedNavBarStaticValue ? 0 : undefined;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
variant={isStaticNav ? 'permanent' : 'persistent'}
|
||||
open={isVisible || isStaticNav}
|
||||
transitionDuration={drawerTransitionDuration}
|
||||
SlideProps={{
|
||||
unmountOnExit: true,
|
||||
}}
|
||||
PaperProps={{
|
||||
ref: (ref: HTMLDivElement | null) => setNavBarElement(ref),
|
||||
}}
|
||||
>
|
||||
<ReaderNavContainer sx={{ backgroundColor: 'background.paper', pointerEvents: 'all' }}>
|
||||
<Stack sx={{ p: 2, gap: 2, backgroundColor: 'action.hover' }}>
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<ReaderExitButton />
|
||||
<CustomTooltip title={t('reader.settings.label.static_navigation')}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setReaderNavBarWidth(0);
|
||||
updateReaderSettings('isStaticNav', !isStaticNav);
|
||||
}}
|
||||
color={isStaticNav ? 'primary' : 'inherit'}
|
||||
>
|
||||
{isStaticNav ? <PushPinIcon /> : <PushPinOutlinedIcon />}
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
</Stack>
|
||||
{manga && currentChapter ? (
|
||||
<>
|
||||
<ReaderNavBarDesktopMetadata
|
||||
mangaId={manga.id}
|
||||
mangaTitle={manga.title}
|
||||
chapterTitle={currentChapter.name}
|
||||
scanlator={currentChapter.scanlator}
|
||||
/>
|
||||
<ReaderNavBarDesktopActions />
|
||||
</>
|
||||
) : (
|
||||
<LoadingPlaceholder />
|
||||
)}
|
||||
</Stack>
|
||||
<Stack sx={{ p: 2, gap: 2 }}>
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<ReaderNavBarDesktopPageNavigation />
|
||||
<ReaderNavBarDesktopChapterNavigation
|
||||
chapters={chapters}
|
||||
currentChapter={currentChapter}
|
||||
nextChapter={nextChapter}
|
||||
previousChapter={previousChapter}
|
||||
/>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<ReaderNavBarDesktopQuickSettings openSettings={openSettings} />
|
||||
</Stack>
|
||||
</ReaderNavContainer>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderNavBarDesktop = withPropsFrom(
|
||||
memo(BaseReaderNavBarDesktop),
|
||||
[
|
||||
useNavBarContext,
|
||||
useReaderStateMangaContext,
|
||||
useReaderStateChaptersContext,
|
||||
userReaderStatePagesContext,
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
],
|
||||
['setReaderNavBarWidth', 'manga', 'chapters', 'currentChapter', 'previousChapter', 'nextChapter', 'isStaticNav'],
|
||||
);
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { Chapters } from '@/features/chapter/services/Chapters.ts';
|
||||
import { ReaderStateChapters } from '@/features/reader/Reader.types.ts';
|
||||
import { DownloadStateIndicator } from '@/features/core/components/downloads/DownloadStateIndicator.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
import { ReaderLibraryButton } from '@/features/reader/overlay/navigation/components/ReaderLibraryButton.tsx';
|
||||
import { ReaderBookmarkButton } from '@/features/reader/overlay/navigation/components/ReaderBookmarkButton.tsx';
|
||||
import { CHAPTER_ACTION_TO_TRANSLATION, FALLBACK_CHAPTER } from '@/features/chapter/Chapter.constants.ts';
|
||||
import { IconBrowser } from '@/assets/icons/IconBrowser.tsx';
|
||||
import { IconWebView } from '@/assets/icons/IconWebView.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
|
||||
const DownloadButton = ({ currentChapter }: Required<Pick<ReaderStateChapters, 'currentChapter'>>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const downloadStatus = Chapters.useDownloadStatusFromCache(currentChapter?.id ?? -1);
|
||||
|
||||
if (currentChapter && Chapters.isDownloaded(currentChapter)) {
|
||||
return (
|
||||
<CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION.delete.action.single)}>
|
||||
<IconButton onClick={() => Chapters.performAction('delete', [currentChapter.id], {})} color="inherit">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (downloadStatus) {
|
||||
return <DownloadStateIndicator chapterId={downloadStatus.chapter.id} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomTooltip title={t(CHAPTER_ACTION_TO_TRANSLATION.download.action.single)} disabled={!currentChapter}>
|
||||
<IconButton
|
||||
disabled={!currentChapter}
|
||||
onClick={() => Chapters.performAction('download', [currentChapter?.id ?? -1], {})}
|
||||
color="inherit"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BaseReaderNavBarDesktopActions = memo(
|
||||
({
|
||||
currentChapter,
|
||||
pageLoadStates,
|
||||
setPageLoadStates,
|
||||
setRetryFailedPagesKeyPrefix,
|
||||
}: Required<Pick<ReaderStateChapters, 'currentChapter'>> &
|
||||
Pick<ReaderStatePages, 'pageLoadStates' | 'setPageLoadStates' | 'setRetryFailedPagesKeyPrefix'>) => {
|
||||
const { id, isBookmarked, realUrl } = currentChapter ?? FALLBACK_CHAPTER;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pageRetryKeyPrefix = useRef<number>(0);
|
||||
|
||||
const haveSomePagesFailedToLoad = useMemo(
|
||||
() => pageLoadStates.some((pageLoadState) => pageLoadState.error),
|
||||
[pageLoadStates],
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', justifyContent: 'center', gap: 1 }}>
|
||||
<ReaderLibraryButton />
|
||||
<ReaderBookmarkButton id={id} isBookmarked={isBookmarked} />
|
||||
<CustomTooltip title={t('reader.button.retry_load_pages')} disabled={!haveSomePagesFailedToLoad}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setPageLoadStates((statePageLoadStates) =>
|
||||
statePageLoadStates.map((pageLoadState) => ({
|
||||
url: pageLoadState.url,
|
||||
loaded: pageLoadState.loaded,
|
||||
})),
|
||||
);
|
||||
setRetryFailedPagesKeyPrefix(`${pageRetryKeyPrefix.current}`);
|
||||
pageRetryKeyPrefix.current = (pageRetryKeyPrefix.current + 1) % 1000;
|
||||
}}
|
||||
disabled={!haveSomePagesFailedToLoad}
|
||||
color="inherit"
|
||||
>
|
||||
<ReplayIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<DownloadButton currentChapter={currentChapter} />
|
||||
<CustomTooltip title={t('global.button.open_browser')} disabled={!realUrl}>
|
||||
<IconButton
|
||||
disabled={!realUrl}
|
||||
href={realUrl ?? ''}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
color="inherit"
|
||||
>
|
||||
<IconBrowser />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('global.button.open_webview')} disabled={!realUrl}>
|
||||
<IconButton
|
||||
disabled={!realUrl}
|
||||
href={realUrl ? requestManager.getWebviewUrl(realUrl) : ''}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
color="inherit"
|
||||
>
|
||||
<IconWebView />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ReaderNavBarDesktopActions = withPropsFrom(
|
||||
BaseReaderNavBarDesktopActions,
|
||||
[useReaderStateChaptersContext, userReaderStatePagesContext],
|
||||
['currentChapter', 'pageLoadStates', 'setPageLoadStates', 'setRetryFailedPagesKeyPrefix'],
|
||||
);
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Box from '@mui/material/Box';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { memo, useLayoutEffect } from 'react';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { bindPopover, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Select } from '@/features/core/components/inputs/Select.tsx';
|
||||
import { ReaderChapterList } from '@/features/reader/overlay/navigation/components/ReaderChapterList.tsx';
|
||||
import { ReaderNavBarDesktopNextPreviousButton } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopNextPreviousButton.tsx';
|
||||
import { getOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import { ReaderStateChapters } from '@/features/reader/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
|
||||
const BaseReaderNavBarDesktopChapterNavigation = ({
|
||||
currentChapter,
|
||||
previousChapter,
|
||||
nextChapter,
|
||||
chapters = [],
|
||||
readerThemeDirection,
|
||||
openChapter,
|
||||
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter' | 'previousChapter' | 'nextChapter'> & {
|
||||
readerThemeDirection: ReturnType<typeof ReaderService.useGetThemeDirection>;
|
||||
openChapter: ReturnType<typeof ReaderControls.useOpenChapter>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const popupState = usePopupState({ variant: 'popover', popupId: 'reader-nav-bar-desktop-chapter-list' });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
popupState.close();
|
||||
}, [currentChapter?.id]);
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
type="previous"
|
||||
title={t(
|
||||
getOptionForDirection(
|
||||
'reader.button.previous_chapter',
|
||||
'reader.button.next_chapter',
|
||||
readerThemeDirection,
|
||||
),
|
||||
)}
|
||||
onClick={() => {
|
||||
openChapter(getOptionForDirection('previous', 'next', readerThemeDirection));
|
||||
}}
|
||||
disabled={getOptionForDirection(!previousChapter, !nextChapter, readerThemeDirection)}
|
||||
/>
|
||||
<FormControl sx={{ flexBasis: '70%', flexGrow: 0, flexShrink: 0 }}>
|
||||
<InputLabel id="reader-nav-bar-desktop-chapter-select">{t('chapter.title_one')}</InputLabel>
|
||||
<Select
|
||||
{...bindTrigger(popupState)}
|
||||
open={popupState.isOpen}
|
||||
value={currentChapter?.id ?? 0}
|
||||
// hide actual select menu
|
||||
MenuProps={{ sx: { visibility: 'hidden' } }}
|
||||
label={t('chapter.title_one')}
|
||||
labelId="reader-nav-bar-desktop-chapter-select"
|
||||
>
|
||||
{/* hacky way to use the select component with a custom menu, the only possible value that is needed is the current chapter */}
|
||||
<MenuItem key={currentChapter?.id} value={currentChapter?.id ?? 0}>
|
||||
{currentChapter ? `#${currentChapter.chapterNumber} ${currentChapter.name}` : ''}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
component={Link}
|
||||
type="next"
|
||||
title={t(
|
||||
getOptionForDirection(
|
||||
'reader.button.next_chapter',
|
||||
'reader.button.previous_chapter',
|
||||
readerThemeDirection,
|
||||
),
|
||||
)}
|
||||
onClick={() => {
|
||||
openChapter(getOptionForDirection('next', 'previous', readerThemeDirection));
|
||||
}}
|
||||
disabled={getOptionForDirection(!nextChapter, !previousChapter, readerThemeDirection)}
|
||||
/>
|
||||
<Popover
|
||||
{...bindPopover(popupState)}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<ReaderChapterList
|
||||
style={{
|
||||
width: '500px',
|
||||
maxWidth: '90vw',
|
||||
minHeight: '150px',
|
||||
maxHeight: '300px',
|
||||
}}
|
||||
currentChapter={currentChapter}
|
||||
chapters={chapters}
|
||||
/>
|
||||
</Box>
|
||||
</Popover>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderNavBarDesktopChapterNavigation = withPropsFrom(
|
||||
memo(BaseReaderNavBarDesktopChapterNavigation),
|
||||
[
|
||||
() => ({
|
||||
readerThemeDirection: ReaderService.useGetThemeDirection(),
|
||||
}),
|
||||
() => ({
|
||||
openChapter: ReaderControls.useOpenChapter(),
|
||||
}),
|
||||
],
|
||||
['readerThemeDirection', 'openChapter'],
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Link from '@mui/material/Link';
|
||||
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { memo } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { TypographyMaxLines } from '@/features/core/components/texts/TypographyMaxLines.tsx';
|
||||
import { AppRoutes } from '@/features/core/AppRoute.constants.ts';
|
||||
|
||||
export const ReaderNavBarDesktopMetadata = memo(
|
||||
({
|
||||
mangaId,
|
||||
mangaTitle,
|
||||
chapterTitle,
|
||||
scanlator,
|
||||
}: {
|
||||
mangaId: number;
|
||||
mangaTitle: string;
|
||||
chapterTitle: string;
|
||||
scanlator?: string | null;
|
||||
}) => (
|
||||
<Stack>
|
||||
<CustomTooltip title={mangaTitle} placement="right">
|
||||
<TypographyMaxLines lines={3} variant="h6" component="h1" sx={{ textAlign: 'center' }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={AppRoutes.manga.path(mangaId)}
|
||||
sx={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
{mangaTitle}
|
||||
</Link>
|
||||
</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={chapterTitle} placement="right">
|
||||
<TypographyMaxLines lines={4} variant="body1" component="h2" sx={{ textAlign: 'center' }}>
|
||||
{chapterTitle}
|
||||
</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
{scanlator && (
|
||||
<CustomTooltip title={scanlator} placement="right">
|
||||
<TypographyMaxLines
|
||||
lines={4}
|
||||
variant="body2"
|
||||
component="h3"
|
||||
color="textDisabled"
|
||||
sx={{ textAlign: 'center' }}
|
||||
>
|
||||
{scanlator}
|
||||
</TypographyMaxLines>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
</Stack>
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { ComponentProps } from 'react';
|
||||
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { CustomButtonIcon } from '@/features/core/components/buttons/CustomButtonIcon.tsx';
|
||||
|
||||
export const ReaderNavBarDesktopNextPreviousButton = ({
|
||||
title,
|
||||
type,
|
||||
disabled,
|
||||
...customIconButtonProps
|
||||
}: Omit<ComponentProps<typeof CustomButtonIcon>, 'children'> & {
|
||||
title: string;
|
||||
type: 'previous' | 'next';
|
||||
}) => (
|
||||
<CustomTooltip title={title} disabled={disabled}>
|
||||
<CustomButtonIcon sx={{ flexBasis: '15%' }} variant="contained" disabled={disabled} {...customIconButtonProps}>
|
||||
{type === 'previous' ? <KeyboardArrowLeftIcon /> : <KeyboardArrowRightIcon />}
|
||||
</CustomButtonIcon>
|
||||
</CustomTooltip>
|
||||
);
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { memo, useMemo } from 'react';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import { Select } from '@/features/core/components/inputs/Select.tsx';
|
||||
import { getNextIndexFromPage, getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { ReaderStatePages } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.types.ts';
|
||||
import { ReaderControls } from '@/features/reader/services/ReaderControls.ts';
|
||||
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { ReaderNavBarDesktopNextPreviousButton } from '@/features/reader/overlay/navigation/desktop/components/ReaderNavBarDesktopNextPreviousButton.tsx';
|
||||
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { userReaderStatePagesContext } from '@/features/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||
|
||||
const BaseReaderNavBarDesktopPageNavigation = ({
|
||||
currentPageIndex,
|
||||
pages,
|
||||
readingDirection,
|
||||
openPage,
|
||||
}: Pick<ReaderStatePages, 'currentPageIndex' | 'pages'> &
|
||||
Pick<IReaderSettings, 'readingDirection'> & {
|
||||
openPage: ReturnType<typeof ReaderControls.useOpenPage>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const getOptionForDirection = useGetOptionForDirection();
|
||||
const currentPage = useMemo(() => getPage(currentPageIndex, pages), [currentPageIndex, pages]);
|
||||
|
||||
const direction = READING_DIRECTION_TO_THEME_DIRECTION[readingDirection];
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }} dir="ltr">
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
type="previous"
|
||||
title={t(getOptionForDirection('reader.button.previous_page', 'reader.button.next_page', direction))}
|
||||
disabled={getOptionForDirection(
|
||||
!currentPage.primary.index,
|
||||
getNextIndexFromPage(currentPage) === getNextIndexFromPage(pages.slice(-1)[0]),
|
||||
direction,
|
||||
)}
|
||||
onClick={() => openPage('previous', undefined, false)}
|
||||
/>
|
||||
<FormControl sx={{ flexBasis: '70%', flexGrow: 0, flexShrink: 0 }}>
|
||||
<InputLabel id="reader-nav-bar-desktop-page-select">{t('reader.page_info.label.page')}</InputLabel>
|
||||
<Select
|
||||
labelId="reader-nav-bar-desktop-page-select"
|
||||
label={t('reader.page_info.label.page')}
|
||||
value={getNextIndexFromPage(currentPage)}
|
||||
onChange={(e) => openPage(e.target.value as number, undefined, false)}
|
||||
>
|
||||
{pages.map((page) => (
|
||||
<MenuItem key={getNextIndexFromPage(page)} value={getNextIndexFromPage(page)}>
|
||||
{page.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<ReaderNavBarDesktopNextPreviousButton
|
||||
type="next"
|
||||
title={t(getOptionForDirection('reader.button.next_page', 'reader.button.previous_page', direction))}
|
||||
disabled={getOptionForDirection(
|
||||
getNextIndexFromPage(currentPage) === getNextIndexFromPage(pages.slice(-1)[0]),
|
||||
!currentPage.primary.index,
|
||||
direction,
|
||||
)}
|
||||
onClick={() => openPage('next', undefined, false)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderNavBarDesktopPageNavigation = withPropsFrom(
|
||||
memo(BaseReaderNavBarDesktopPageNavigation),
|
||||
[
|
||||
userReaderStatePagesContext,
|
||||
() => ({ openPage: ReaderControls.useOpenPage() }),
|
||||
ReaderService.useSettingsWithoutDefaultFlag,
|
||||
],
|
||||
['currentPageIndex', 'pages', 'readingDirection', 'openPage'],
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
export const ReaderNavContainer = styled(Stack)({
|
||||
width: '400px',
|
||||
minWidth: '400px',
|
||||
maxWidth: '400px',
|
||||
height: '100vh',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import { ReaderNavBarDesktopPageScale } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopPageScale.tsx';
|
||||
import { ReaderNavBarDesktopReadingMode } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopReadingMode.tsx';
|
||||
import { ReaderNavBarDesktopOffsetDoubleSpread } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopOffsetDoubleSpread.tsx';
|
||||
import { ReaderNavBarDesktopReadingDirection } from '@/features/reader/overlay/navigation/desktop/quick-settings/components/ReaderNavBarDesktopReadingDirection.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, TReaderStateMangaContext } from '@/features/reader/Reader.types.ts';
|
||||
import { ReaderNavBarDesktopProps } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { ReaderNavBarDesktopAutoScroll } from '@/features/reader/auto-scroll/settings/quick-setting/ReaderNavBarDesktopAutoScroll.tsx';
|
||||
|
||||
const BaseReaderNavBarDesktopQuickSettings = ({
|
||||
manga,
|
||||
readingMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
readingDirection,
|
||||
autoScroll,
|
||||
openSettings,
|
||||
}: Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<ReaderNavBarDesktopProps, 'openSettings'> &
|
||||
Pick<
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
| 'readingMode'
|
||||
| 'shouldOffsetDoubleSpreads'
|
||||
| 'pageScaleMode'
|
||||
| 'shouldStretchPage'
|
||||
| 'readingDirection'
|
||||
| 'autoScroll'
|
||||
>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateSetting = ReaderService.useCreateUpdateSetting(manga ?? FALLBACK_MANGA);
|
||||
const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? FALLBACK_MANGA);
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<ReaderNavBarDesktopReadingMode
|
||||
readingMode={readingMode}
|
||||
setReadingMode={(value) => updateSetting('readingMode', value)}
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('readingMode')}
|
||||
/>
|
||||
<ReaderNavBarDesktopOffsetDoubleSpread
|
||||
readingMode={readingMode.value}
|
||||
shouldOffsetDoubleSpreads={shouldOffsetDoubleSpreads.value}
|
||||
setShouldOffsetDoubleSpreads={(value) => updateSetting('shouldOffsetDoubleSpreads', value)}
|
||||
/>
|
||||
<ReaderNavBarDesktopPageScale
|
||||
pageScaleMode={pageScaleMode}
|
||||
shouldStretchPage={shouldStretchPage}
|
||||
updateSetting={updateSetting}
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('pageScaleMode')}
|
||||
/>
|
||||
<ReaderNavBarDesktopReadingDirection
|
||||
readingDirection={readingDirection}
|
||||
setReadingDirection={(value) => updateSetting('readingDirection', value)}
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('readingDirection')}
|
||||
/>
|
||||
<ReaderNavBarDesktopAutoScroll
|
||||
autoScroll={autoScroll}
|
||||
setAutoScroll={(...args) => updateSetting('autoScroll', ...args)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => openSettings()}
|
||||
size="large"
|
||||
sx={{ justifyContent: 'start', textTransform: 'none' }}
|
||||
variant="contained"
|
||||
startIcon={<SettingsIcon />}
|
||||
>
|
||||
{t('settings.title')}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderNavBarDesktopQuickSettings = withPropsFrom(
|
||||
BaseReaderNavBarDesktopQuickSettings,
|
||||
[useReaderStateMangaContext, ReaderService.useSettings],
|
||||
[
|
||||
'manga',
|
||||
'readingMode',
|
||||
'shouldOffsetDoubleSpreads',
|
||||
'pageScaleMode',
|
||||
'shouldStretchPage',
|
||||
'readingDirection',
|
||||
'autoScroll',
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { OffsetDoubleSpreadIcon } from '@/assets/icons/svg/OffsetDoubleSpreadIcon.tsx';
|
||||
import { IReaderSettings } from '@/features/reader/Reader.types.ts';
|
||||
import { isOffsetDoubleSpreadPagesEditable } from '@/features/reader/settings/ReaderSettings.utils.tsx';
|
||||
|
||||
export const ReaderNavBarDesktopOffsetDoubleSpread = ({
|
||||
readingMode,
|
||||
shouldOffsetDoubleSpreads,
|
||||
setShouldOffsetDoubleSpreads,
|
||||
}: Pick<IReaderSettings, 'readingMode' | 'shouldOffsetDoubleSpreads'> & {
|
||||
setShouldOffsetDoubleSpreads: (offset: boolean) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isOffsetDoubleSpreadPagesEditable(readingMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
sx={{ justifyContent: 'start', textTransform: 'unset' }}
|
||||
size="large"
|
||||
onClick={() => setShouldOffsetDoubleSpreads(!shouldOffsetDoubleSpreads)}
|
||||
color={shouldOffsetDoubleSpreads ? 'secondary' : 'primary'}
|
||||
variant="contained"
|
||||
startIcon={<OffsetDoubleSpreadIcon />}
|
||||
>
|
||||
{t('reader.settings.label.offset_double_spread')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FitScreenIcon from '@mui/icons-material/FitScreen';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { ValueRotationButton } from '@/features/core/components/buttons/ValueRotationButton.tsx';
|
||||
import {
|
||||
IReaderSettings,
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
ReaderPageScaleMode,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import {
|
||||
PAGE_SCALE_VALUE_TO_DISPLAY_DATA,
|
||||
READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED,
|
||||
READER_PAGE_SCALE_MODE_VALUES,
|
||||
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
import { CustomButtonIcon } from '@/features/core/components/buttons/CustomButtonIcon.tsx';
|
||||
|
||||
export const ReaderNavBarDesktopPageScale = ({
|
||||
pageScaleMode,
|
||||
shouldStretchPage,
|
||||
updateSetting,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'pageScaleMode' | 'shouldStretchPage'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReaderPageScaleMode>, 'isDefaultable' | 'onDefault'> & {
|
||||
updateSetting: <Setting extends keyof Pick<IReaderSettings, 'pageScaleMode' | 'shouldStretchPage'>>(
|
||||
setting: Setting,
|
||||
value: IReaderSettings[Setting],
|
||||
) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack sx={{ flexDirection: 'row', gap: 1 }}>
|
||||
<ValueRotationButton
|
||||
{...buttonSelectInputProps}
|
||||
tooltip={t('reader.settings.page_scale.title')}
|
||||
value={pageScaleMode.isDefault ? undefined : pageScaleMode.value}
|
||||
defaultValue={pageScaleMode.isDefault ? pageScaleMode.value : undefined}
|
||||
values={READER_PAGE_SCALE_MODE_VALUES}
|
||||
setValue={(value) => updateSetting('pageScaleMode', value)}
|
||||
valueToDisplayData={PAGE_SCALE_VALUE_TO_DISPLAY_DATA}
|
||||
defaultIcon={PAGE_SCALE_VALUE_TO_DISPLAY_DATA[pageScaleMode.value].icon}
|
||||
/>
|
||||
{READER_PAGE_SCALE_MODE_TO_SCALING_ALLOWED[pageScaleMode.value] && (
|
||||
<CustomTooltip title={t('reader.settings.page_scale.stretch')}>
|
||||
<CustomButtonIcon
|
||||
onClick={() => updateSetting('shouldStretchPage', !shouldStretchPage.value)}
|
||||
sx={{ px: undefined }}
|
||||
variant="contained"
|
||||
color={shouldStretchPage.value ? 'secondary' : 'primary'}
|
||||
>
|
||||
<FitScreenIcon />
|
||||
</CustomButtonIcon>
|
||||
</CustomTooltip>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ValueRotationButton } from '@/features/core/components/buttons/ValueRotationButton.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingDirection } from '@/features/reader/Reader.types.ts';
|
||||
import {
|
||||
READING_DIRECTION_VALUES,
|
||||
READING_DIRECTION_VALUE_TO_DISPLAY_DATA,
|
||||
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
|
||||
export const ReaderNavBarDesktopReadingDirection = ({
|
||||
readingDirection,
|
||||
setReadingDirection,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'readingDirection'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingDirection>, 'isDefaultable' | 'onDefault'> & {
|
||||
setReadingDirection: (readingDirection: ReadingDirection) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ValueRotationButton
|
||||
{...buttonSelectInputProps}
|
||||
tooltip={t('reader.settings.label.reading_direction')}
|
||||
value={readingDirection.isDefault ? undefined : readingDirection.value}
|
||||
defaultValue={readingDirection.isDefault ? readingDirection.value : undefined}
|
||||
values={READING_DIRECTION_VALUES}
|
||||
setValue={setReadingDirection}
|
||||
valueToDisplayData={READING_DIRECTION_VALUE_TO_DISPLAY_DATA}
|
||||
defaultIcon={READING_DIRECTION_VALUE_TO_DISPLAY_DATA[readingDirection.value].icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ValueRotationButton } from '@/features/core/components/buttons/ValueRotationButton.tsx';
|
||||
import { IReaderSettingsWithDefaultFlag, ReadingMode } from '@/features/reader/Reader.types.ts';
|
||||
import {
|
||||
READING_MODE_VALUE_TO_DISPLAY_DATA,
|
||||
READING_MODE_VALUES,
|
||||
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { MultiValueButtonDefaultableProps } from '@/features/core/Core.types.ts';
|
||||
|
||||
export const ReaderNavBarDesktopReadingMode = ({
|
||||
readingMode,
|
||||
setReadingMode,
|
||||
...buttonSelectInputProps
|
||||
}: Pick<IReaderSettingsWithDefaultFlag, 'readingMode'> &
|
||||
Pick<MultiValueButtonDefaultableProps<ReadingMode>, 'isDefaultable' | 'onDefault'> & {
|
||||
setReadingMode: (mode: ReadingMode) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ValueRotationButton
|
||||
{...buttonSelectInputProps}
|
||||
tooltip={t('reader.settings.label.reading_mode')}
|
||||
value={readingMode.isDefault ? undefined : readingMode.value}
|
||||
defaultValue={readingMode.isDefault ? readingMode.value : undefined}
|
||||
values={READING_MODE_VALUES}
|
||||
setValue={setReadingMode}
|
||||
valueToDisplayData={READING_MODE_VALUE_TO_DISPLAY_DATA}
|
||||
defaultIcon={READING_MODE_VALUE_TO_DISPLAY_DATA[readingMode.value].icon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import AppSettingsAltIcon from '@mui/icons-material/AppSettingsAlt';
|
||||
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
|
||||
import { alpha } from '@mui/material/styles';
|
||||
import { bindDialog, bindTrigger, usePopupState } from 'material-ui-popup-state/hooks';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import { memo, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { CustomTooltip } from '@/features/core/components/CustomTooltip.tsx';
|
||||
import { ReaderBottomBarMobileProps } from '@/features/reader/overlay/ReaderOverlay.types.ts';
|
||||
import { MobileReaderProgressBar } from '@/features/reader/overlay/progress-bar/mobile/MobileReaderProgressBar.tsx';
|
||||
import { ReaderChapterList } from '@/features/reader/overlay/navigation/components/ReaderChapterList.tsx';
|
||||
import { ReaderBottomBarMobileQuickSettings } from '@/features/reader/overlay/navigation/mobile/quick-settings/ReaderBottomBarMobileQuickSettings.tsx';
|
||||
import { useReaderStateChaptersContext } from '@/features/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||
import { useReaderScrollbarContext } from '@/features/reader/contexts/ReaderScrollbarContext.tsx';
|
||||
import { ReaderStateChapters, TReaderScrollbarContext } from '@/features/reader/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { useResizeObserver } from '@/features/core/hooks/useResizeObserver.tsx';
|
||||
|
||||
const BaseReaderBottomBarMobile = ({
|
||||
openSettings,
|
||||
isVisible,
|
||||
currentChapter,
|
||||
chapters,
|
||||
scrollbarXSize,
|
||||
scrollbarYSize,
|
||||
topOffset = 0,
|
||||
}: ReaderBottomBarMobileProps &
|
||||
Pick<ReaderStateChapters, 'currentChapter' | 'chapters'> &
|
||||
Pick<TReaderScrollbarContext, 'scrollbarXSize' | 'scrollbarYSize'> & { topOffset?: number }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const chapterListPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-chapter-list-dialog' });
|
||||
const quickSettingsPopupState = usePopupState({ variant: 'dialog', popupId: 'reader-quick-settings-dialog' });
|
||||
|
||||
const [bottomBarRefHeight, setBottomBarRefHeight] = useState(0);
|
||||
const bottomBarRef = useRef<HTMLDivElement>(null);
|
||||
useResizeObserver(
|
||||
bottomBarRef,
|
||||
useCallback(() => setBottomBarRefHeight(bottomBarRef.current?.clientHeight ?? 0), [bottomBarRefHeight]),
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
chapterListPopupState.close();
|
||||
}, [currentChapter?.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
right: `${scrollbarYSize}px`,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: `calc(100% - ${topOffset}px)`,
|
||||
}}
|
||||
>
|
||||
<MobileReaderProgressBar topOffset={topOffset} bottomOffset={bottomBarRefHeight} />
|
||||
<Slide direction="up" in={isVisible}>
|
||||
<Stack
|
||||
ref={bottomBarRef}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.95),
|
||||
pb: `max(${scrollbarXSize}px, env(safe-area-inset-bottom))`,
|
||||
boxShadow: 2,
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
sx={{
|
||||
width: '50%',
|
||||
flexDirection: 'row',
|
||||
p: 2,
|
||||
gap: 1,
|
||||
justifyContent: 'space-evenly',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<CustomTooltip title={t('reader.button.chapter_list')}>
|
||||
<IconButton {...bindTrigger(chapterListPopupState)} color="inherit">
|
||||
<FormatListBulletedIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('reader.settings.title.quick_settings')}>
|
||||
<IconButton {...bindTrigger(quickSettingsPopupState)} color="inherit">
|
||||
<AppSettingsAltIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
<CustomTooltip title={t('settings.title')}>
|
||||
<IconButton onClick={openSettings} color="inherit">
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</CustomTooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Slide>
|
||||
</Stack>
|
||||
{chapterListPopupState.isOpen && (
|
||||
<Dialog {...bindDialog(chapterListPopupState)} fullWidth maxWidth="md" scroll="paper">
|
||||
<DialogContent sx={{ p: 0, pb: 1 }}>
|
||||
<ReaderChapterList
|
||||
style={{
|
||||
minHeight: '15vh',
|
||||
maxHeight: '75vh',
|
||||
}}
|
||||
currentChapter={currentChapter}
|
||||
chapters={chapters}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{quickSettingsPopupState.isOpen && (
|
||||
<Dialog {...bindDialog(quickSettingsPopupState)} fullWidth maxWidth="md" scroll="paper">
|
||||
<DialogContent>
|
||||
<ReaderBottomBarMobileQuickSettings />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderBottomBarMobile = withPropsFrom(
|
||||
memo(BaseReaderBottomBarMobile),
|
||||
[useReaderStateChaptersContext, useReaderScrollbarContext],
|
||||
['currentChapter', 'chapters', 'scrollbarXSize', 'scrollbarYSize'],
|
||||
);
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReaderSettingReadingMode } from '@/features/reader/settings/layout/components/ReaderSettingReadingMode.tsx';
|
||||
import { ReaderSettingReadingDirection } from '@/features/reader/settings/layout/components/ReaderSettingReadingDirection.tsx';
|
||||
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
|
||||
import { useReaderStateMangaContext } from '@/features/reader/contexts/state/ReaderStateMangaContext.tsx';
|
||||
import { DefaultSettingFootnote } from '@/features/reader/settings/components/DefaultSettingFootnote.tsx';
|
||||
import {
|
||||
IReaderSettingsWithDefaultFlag,
|
||||
TReaderAutoScrollContext,
|
||||
TReaderStateMangaContext,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { withPropsFrom } from '@/features/core/hoc/withPropsFrom.tsx';
|
||||
import { FALLBACK_MANGA } from '@/features/manga/Manga.constants.ts';
|
||||
import { ReaderSettingAutoScroll } from '@/features/reader/auto-scroll/settings/ReaderSettingAutoScroll.tsx';
|
||||
import { CheckboxInput } from '@/features/core/components/inputs/CheckboxInput.tsx';
|
||||
import { useReaderAutoScrollContext } from '@/features/reader/auto-scroll/contexts/ReaderAutoScrollContext.tsx';
|
||||
|
||||
const BaseReaderBottomBarMobileQuickSettings = ({
|
||||
manga,
|
||||
readingMode,
|
||||
readingDirection,
|
||||
autoScroll,
|
||||
isActive,
|
||||
toggleActive,
|
||||
}: Pick<TReaderStateMangaContext, 'manga'> &
|
||||
Pick<IReaderSettingsWithDefaultFlag, 'readingMode' | 'readingDirection' | 'autoScroll'> &
|
||||
Pick<TReaderAutoScrollContext, 'isActive' | 'toggleActive'>) => {
|
||||
const { t } = useTranslation();
|
||||
const deleteSetting = ReaderService.useCreateDeleteSetting(manga ?? FALLBACK_MANGA);
|
||||
|
||||
if (!manga) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<DefaultSettingFootnote />
|
||||
<ReaderSettingReadingMode
|
||||
readingMode={readingMode}
|
||||
setReadingMode={(value) => ReaderService.updateSetting(manga, 'readingMode', value)}
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('readingMode')}
|
||||
/>
|
||||
<ReaderSettingReadingDirection
|
||||
readingDirection={readingDirection}
|
||||
setReadingDirection={(value) => ReaderService.updateSetting(manga, 'readingDirection', value)}
|
||||
isDefaultable
|
||||
onDefault={() => deleteSetting('readingDirection')}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('reader.settings.auto_scroll.title')}
|
||||
checked={isActive}
|
||||
onChange={() => toggleActive()}
|
||||
/>
|
||||
<ReaderSettingAutoScroll
|
||||
autoScroll={autoScroll}
|
||||
setAutoScroll={(...args) => ReaderService.updateSetting(manga, 'autoScroll', ...args)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ReaderBottomBarMobileQuickSettings = withPropsFrom(
|
||||
memo(BaseReaderBottomBarMobileQuickSettings),
|
||||
[useReaderStateMangaContext, ReaderService.useSettings, useReaderAutoScrollContext],
|
||||
['manga', 'readingMode', 'readingDirection', 'autoScroll', 'isActive', 'toggleActive'],
|
||||
);
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
|
||||
import ComputerIcon from '@mui/icons-material/Computer';
|
||||
import AutoModeIcon from '@mui/icons-material/AutoMode';
|
||||
import { IReaderSettings, ReaderOverlayMode } from '@/features/reader/Reader.types.ts';
|
||||
import { ValueToDisplayData } from '@/features/core/Core.types.ts';
|
||||
import { ButtonSelectInput } from '@/features/core/components/inputs/ButtonSelectInput.tsx';
|
||||
|
||||
const VALUE_TO_DISPLAY_DATA: ValueToDisplayData<ReaderOverlayMode> = {
|
||||
[ReaderOverlayMode.AUTO]: {
|
||||
title: 'global.label.auto',
|
||||
icon: <AutoModeIcon />,
|
||||
},
|
||||
[ReaderOverlayMode.DESKTOP]: {
|
||||
title: 'global.label.desktop',
|
||||
icon: <ComputerIcon />,
|
||||
},
|
||||
[ReaderOverlayMode.MOBILE]: {
|
||||
title: 'global.label.mobile',
|
||||
icon: <PhoneIphoneIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
const READING_MODE_VALUES = Object.values(ReaderOverlayMode).filter((value) => typeof value === 'number');
|
||||
|
||||
export const ReaderSettingOverlayMode = ({
|
||||
overlayMode,
|
||||
setOverlayMode,
|
||||
}: Pick<IReaderSettings, 'overlayMode'> & {
|
||||
setOverlayMode: (mode: ReaderOverlayMode) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ButtonSelectInput
|
||||
label={t('reader.settings.overlay_mode')}
|
||||
value={overlayMode}
|
||||
values={READING_MODE_VALUES}
|
||||
setValue={setOverlayMode}
|
||||
valueToDisplayData={VALUE_TO_DISPLAY_DATA}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user