Resume chapter at specific page

- open from outside reader
 - is read
  - yes: 1st page
  - no : last read page
- inside reader via
 - chapter selection: first page
 - previous chapter : last page
 - next chapter     : first page
This commit is contained in:
schroda
2024-12-06 23:59:39 +01:00
parent 206c216093
commit 916abd3bb1
15 changed files with 106 additions and 41 deletions

View File

@@ -122,8 +122,6 @@ export const ChapterList = ({
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
const nextChapterIndexToRead = manga.firstUnreadChapter?.sourceOrder;
const areAllChaptersRead = Mangas.isFullyRead(manga);
const areAllChaptersDownloaded = Mangas.isFullyDownloaded(manga);
@@ -157,8 +155,8 @@ export const ChapterList = ({
);
}
if (nextChapterIndexToRead !== undefined) {
return <ResumeFab chapterIndex={nextChapterIndexToRead} mangaId={manga.id} />;
if (manga.firstUnreadChapter) {
return <ResumeFab chapter={manga.firstUnreadChapter} />;
}
return null;

View File

@@ -36,6 +36,7 @@ import {
ChapterMangaInfo,
ChapterNumberInfo,
ChapterReadInfo,
Chapters,
ChapterScanlatorInfo,
} from '@/modules/chapter/services/Chapters.ts';
import { ChaptersWithMeta } from '@/modules/chapter/services/ChaptersWithMeta.ts';
@@ -128,10 +129,11 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
>
<CardActionArea
component={Link}
to={`/manga/${chapter.mangaId}/chapter/${chapter.sourceOrder}`}
to={Chapters.getReaderUrl(chapter)}
style={{
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
}}
state={{ resumeMode: Chapters.getReaderResumeMode(chapter) }}
replace={mode === 'reader'}
onClick={(e) => handleClick(e)}
{...longPressBind(popupState.open)}

View File

@@ -21,6 +21,7 @@ import { CHAPTER_LIST_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts
import { DirectionOffset, TranslationKey } from '@/Base.types.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
import { ReaderResumeMode } from '@/modules/reader/types/Reader.types.ts';
export type ChapterAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread';
@@ -391,4 +392,12 @@ export class Chapters {
return onlyUnread ? Chapters.getNonRead(nextChapters) : nextChapters;
}
static getReaderResumeMode(chapter: ChapterReadInfo): ReaderResumeMode {
if (chapter.isRead) {
return ReaderResumeMode.START;
}
return ReaderResumeMode.LAST_READ;
}
}

View File

@@ -20,7 +20,7 @@ export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.s
type MangaCardBaseProps = Pick<MangaType, 'id' | 'title' | 'sourceId'> &
Omit<SingleModeProps['manga'], 'downloadCount' | 'unreadCount' | 'chapters'> &
Partial<Pick<MangaType, 'inLibrary' | 'downloadCount' | 'unreadCount'>> & {
firstUnreadChapter?: Pick<ChapterType, 'id' | 'sourceOrder'> | null;
firstUnreadChapter?: Pick<ChapterType, 'id' | 'sourceOrder' | 'isRead'> | null;
};
export type MangaIdInfo = Pick<MangaType, 'id'>;

View File

@@ -12,24 +12,26 @@ import Tooltip from '@mui/material/Tooltip';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import { Link } from 'react-router-dom';
import { ChapterReadInfo, Chapters, ChapterSourceOrderInfo } from '@/modules/chapter/services/Chapters.ts';
export const ContinueReadingButton = ({
showContinueReadingButton,
nextChapterIndexToRead,
chapter,
mangaLinkTo,
}: {
showContinueReadingButton: boolean;
nextChapterIndexToRead?: number;
chapter?: (ChapterSourceOrderInfo & ChapterReadInfo) | null;
mangaLinkTo: string;
}) => {
const { t } = useTranslation();
const isFirstChapter = nextChapterIndexToRead === 1;
if (!showContinueReadingButton || nextChapterIndexToRead === undefined) {
if (!showContinueReadingButton || !chapter) {
return null;
}
const { sourceOrder } = chapter;
const isFirstChapter = sourceOrder === 1;
return (
<Tooltip title={t(isFirstChapter ? 'global.button.start' : 'global.button.resume')}>
<Button
@@ -37,7 +39,8 @@ export const ContinueReadingButton = ({
size="small"
sx={{ minWidth: 'unset', py: 0.5, px: 0.75 }}
component={Link}
to={`${mangaLinkTo}chapter/${nextChapterIndexToRead}`}
to={`${mangaLinkTo}chapter/${chapter.sourceOrder}`}
state={{ resumeMode: Chapters.getReaderResumeMode(chapter) }}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>

View File

@@ -10,20 +10,27 @@ import { Link } from 'react-router-dom';
import PlayArrow from '@mui/icons-material/PlayArrow';
import { useTranslation } from 'react-i18next';
import { StyledFab } from '@/modules/core/components/buttons/StyledFab.tsx';
import {
ChapterMangaInfo,
ChapterReadInfo,
Chapters,
ChapterSourceOrderInfo,
} from '@/modules/chapter/services/Chapters.ts';
interface ResumeFABProps {
chapterIndex: number;
mangaId: number;
}
export function ResumeFab(props: ResumeFABProps) {
export function ResumeFab({ chapter }: { chapter: ChapterMangaInfo & ChapterSourceOrderInfo & ChapterReadInfo }) {
const { t } = useTranslation();
const { chapterIndex, mangaId } = props;
const { sourceOrder } = chapter;
return (
<StyledFab component={Link} variant="extended" color="primary" to={`/manga/${mangaId}/chapter/${chapterIndex}`}>
<StyledFab
component={Link}
variant="extended"
color="primary"
to={Chapters.getReaderUrl(chapter)}
state={{ resumeMode: Chapters.getReaderResumeMode(chapter) }}
>
<PlayArrow />
{chapterIndex === 1 ? t('global.button.start') : t('global.button.resume')}
{sourceOrder === 1 ? t('global.button.start') : t('global.button.resume')}
</StyledFab>
);
}

View File

@@ -55,8 +55,6 @@ export const MangaCard = (props: MangaCardProps) => {
const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.sourceId, manga.title);
const nextChapterIndexToRead = firstUnreadChapter?.sourceOrder;
const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false);
const handleClick = (event: React.MouseEvent | React.TouchEvent, openMenu?: () => void) => {
@@ -108,11 +106,11 @@ export const MangaCard = (props: MangaCardProps) => {
() => (
<ContinueReadingButton
showContinueReadingButton={showContinueReadingButton && mode === 'default'}
nextChapterIndexToRead={nextChapterIndexToRead}
chapter={firstUnreadChapter}
mangaLinkTo={mangaLinkTo}
/>
),
[showContinueReadingButton, nextChapterIndexToRead, mangaLinkTo],
[showContinueReadingButton, firstUnreadChapter, mangaLinkTo],
);
const mangaBadges = useMemo(

View File

@@ -24,6 +24,7 @@ import { ReaderNavBarDesktopNextPreviousButton } from '@/modules/reader/componen
import { useGetOptionForDirection } from '@/theme.tsx';
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { READING_DIRECTION_TO_THEME_DIRECTION } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { ReaderResumeMode } from '@/modules/reader/types/Reader.types.ts';
export const ReaderNavBarDesktopChapterNavigation = ({
currentChapter,
@@ -61,6 +62,7 @@ export const ReaderNavBarDesktopChapterNavigation = ({
direction,
)}
replace
state={{ resumeMode: getOptionForDirection(ReaderResumeMode.END, ReaderResumeMode.START) }}
/>
<FormControl sx={{ flexBasis: '70%', flexGrow: 0, flexShrink: 0 }}>
<InputLabel id="reader-nav-bar-desktop-chapter-select">{t('chapter.title_one')}</InputLabel>
@@ -92,6 +94,7 @@ export const ReaderNavBarDesktopChapterNavigation = ({
direction,
)}
replace
state={{ resumeMode: getOptionForDirection(ReaderResumeMode.START, ReaderResumeMode.END) }}
/>
<Popover
{...bindPopover(popupState)}

View File

@@ -20,7 +20,7 @@ import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/R
import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { getPage } from '@/modules/reader/utils/ReaderProgressBar.utils.tsx';
import { getOptionForDirection } from '@/theme.tsx';
import { ProgressBarPosition } from '@/modules/reader/types/Reader.types.ts';
import { ProgressBarPosition, ReaderResumeMode } from '@/modules/reader/types/Reader.types.ts';
import { ReaderProgressBarDirectionWrapper } from '@/modules/reader/components/overlay/progress-bar/ReaderProgressBarDirectionWrapper.tsx';
import { useReaderProgressBarContext } from '@/modules/reader/contexts/ReaderProgressBarContext.tsx';
import { useReaderOverlayContext } from '@/modules/reader/contexts/ReaderOverlayContext.tsx';
@@ -34,8 +34,8 @@ export const MobileReaderProgressBar = () => {
const direction = ReaderService.useGetThemeDirection();
const openNextChapter = ReaderService.useNavigateToChapter(nextChapter);
const openPreviousChapter = ReaderService.useNavigateToChapter(previousChapter);
const openNextChapter = ReaderService.useNavigateToChapter(nextChapter, ReaderResumeMode.START);
const openPreviousChapter = ReaderService.useNavigateToChapter(previousChapter, ReaderResumeMode.END);
useLayoutEffect(() => {
setIsMaximized(isVisible);

View File

@@ -15,9 +15,10 @@ import { ReaderService } from '@/modules/reader/services/ReaderService.ts';
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
import {
PageInViewportType,
ReaderResumeMode,
ReaderTransitionPageMode,
ReadingDirection,
ReadingMode,
ReaderTransitionPageMode,
} from '@/modules/reader/types/Reader.types.ts';
import { ScrollDirection, ScrollOffset } from '@/modules/core/Core.types.ts';
import { ReaderScrollAmount } from '@/modules/reader/constants/ReaderSettings.constants.tsx';
@@ -115,8 +116,8 @@ export class ReaderControls {
const { previousChapter, nextChapter } = useReaderStateChaptersContext();
const direction = READING_DIRECTION_TO_DIRECTION[readingDirection.value];
const openPreviousChapter = ReaderService.useNavigateToChapter(previousChapter);
const openNextChapter = ReaderService.useNavigateToChapter(nextChapter);
const openPreviousChapter = ReaderService.useNavigateToChapter(previousChapter, ReaderResumeMode.END);
const openNextChapter = ReaderService.useNavigateToChapter(nextChapter, ReaderResumeMode.START);
return useCallback(
(offset) => {

View File

@@ -17,6 +17,7 @@ import {
IReaderSettingsWithDefaultFlag,
ReaderExitMode,
ReaderOverlayMode,
ReaderResumeMode,
ReadingDirection,
} from '@/modules/reader/types/Reader.types.ts';
import { useReaderStateMangaContext } from '@/modules/reader/contexts/state/ReaderStateMangaContext.tsx';
@@ -44,9 +45,19 @@ const DIRECTION_TO_READING_DIRECTION: Record<Direction, ReadingDirection> = {
};
export class ReaderService {
static useNavigateToChapter(chapter?: TChapterReader): () => void {
static useNavigateToChapter(chapter?: TChapterReader, resumeMode?: ReaderResumeMode): () => void {
const navigate = useNavigate();
return useCallback(() => chapter && navigate(Chapters.getReaderUrl(chapter), { replace: true }), [chapter]);
return useCallback(
() =>
chapter &&
navigate(Chapters.getReaderUrl(chapter), {
replace: true,
state: {
resumeMode,
},
}),
[chapter],
);
}
static useSettings(): IReaderSettingsWithDefaultFlag {

View File

@@ -214,3 +214,9 @@ export enum ReaderTransitionPageMode {
NEXT,
BOTH,
}
export enum ReaderResumeMode {
START,
END,
LAST_READ,
}

View File

@@ -0,0 +1,25 @@
/*
* 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 { ReaderResumeMode } from '@/modules/reader/types/Reader.types.ts';
export const getInitialReaderPageIndex = (
resumeMode: ReaderResumeMode,
lastReadPageIndex: number,
lastPageIndex: number,
): number => {
if (resumeMode === ReaderResumeMode.START) {
return 0;
}
if (resumeMode === ReaderResumeMode.END) {
return lastPageIndex;
}
return Math.max(0, Math.min(lastPageIndex, lastReadPageIndex));
};