Structure "reader" in sub-features

This commit is contained in:
schroda
2025-08-16 02:02:13 +02:00
parent fc12f5dfd3
commit f548ba9502
140 changed files with 435 additions and 429 deletions

View File

@@ -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);

View File

@@ -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}
/>
);
};

View File

@@ -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'],
);

View File

@@ -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'],
);

View File

@@ -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'],
);

View File

@@ -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'],
);

View File

@@ -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'],
);

View File

@@ -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>
),
);

View File

@@ -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>
);

View File

@@ -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'],
);

View File

@@ -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',
});

View File

@@ -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',
],
);

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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}
/>
);
};

View File

@@ -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}
/>
);
};

View File

@@ -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'],
);

View File

@@ -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'],
);