Structure "reader" in sub-features
This commit is contained in:
@@ -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',
|
||||
});
|
||||
Reference in New Issue
Block a user