Show missing chapter info in chapter list

Closes #836
This commit is contained in:
schroda
2025-05-24 20:32:03 +02:00
parent 86578593ca
commit 61471ae214
7 changed files with 164 additions and 16 deletions

View File

@@ -14,7 +14,6 @@ import { ComponentProps, useCallback, useMemo, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { useTranslation } from 'react-i18next';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
import { ResumeFab } from '@/modules/manga/components/ResumeFAB.tsx';
import {
filterAndSortChapters,
@@ -45,6 +44,7 @@ import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { makeToast } from '@/modules/core/utils/Toast.ts';
import { ChapterListCard } from '@/modules/chapter/components/cards/ChapterListCard.tsx';
type ChapterListHeaderProps = {
scrollbarWidth: number;
@@ -145,6 +145,7 @@ export const ChapterList = ({
const visibleChapterIds = useMemo(() => Chapters.getIds(visibleChapters), [visibleChapters]);
const areAllChaptersRead = Mangas.isFullyRead(manga);
const areAllChaptersDownloaded = Mangas.isFullyDownloaded(manga);
const missingChapterCount = useMemo(() => Chapters.getMissingCount(visibleChapters), [visibleChapters]);
const noChaptersFound = chapters.length === 0;
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
@@ -193,11 +194,18 @@ export const ChapterList = ({
justifyContent="space-between"
scrollbarWidth={scrollbarWidth}
>
<Typography variant="h5" component="h3">
{`${visibleChapters.length} ${t('chapter.title_one', {
count: visibleChapters.length,
})}`}
</Typography>
<Stack>
<Typography variant="h5" component="h3">
{t('chapter.value', { count: visibleChapters.length })}
</Typography>
{!!missingChapterCount && (
<Typography variant="body2" color="warning">
{`${t('chapter.missing', {
count: missingChapterCount,
})}`}
</Typography>
)}
</Stack>
<Stack direction="row">
{areNoItemsSelected && (
@@ -235,8 +243,10 @@ export const ChapterList = ({
totalCount={visibleChapters.length}
computeItemKey={(index) => visibleChapters[index].id}
itemContent={(index: number) => (
<ChapterCard
chapter={visibleChapters[index]}
<ChapterListCard
index={index}
isSortDesc={options.reverse}
chapters={visibleChapters}
selected={!areNoItemsSelected ? selectedItemIds.includes(visibleChapters[index].id) : null}
showChapterNumber={options.showChapterNumber}
onSelect={onSelect}

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/.
*/
/*
* 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 Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
export const MissingChaptersInfoSeparator = ({ missingChaptersGap }: { missingChaptersGap: number }) => {
const { t } = useTranslation();
return (
<Stack
sx={{
width: '100%',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
p: 2,
py: 2.5,
}}
>
<Box
sx={{
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
/>
<Typography sx={{ px: 2 }} variant="body2" color="textSecondary">
{t('chapter.missing', { count: missingChaptersGap })}
</Typography>
<Box
sx={{
flexGrow: 1,
border: '1px solid',
borderColor: (theme) => theme.palette.text.secondary,
}}
/>
</Stack>
);
};

View File

@@ -0,0 +1,43 @@
/*
* 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 { ComponentProps } from 'react';
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
import { Chapters } from '@/modules/chapter/services/Chapters.ts';
import { applyStyles } from '@/modules/core/utils/ApplyStyles.ts';
import { MissingChaptersInfoSeparator } from '@/modules/chapter/components/MissingChaptersInfoSeparator.tsx';
type ChapterCardProps = ComponentProps<typeof ChapterCard>;
export const ChapterListCard = ({
index,
isSortDesc,
chapters,
...chapterCardProps
}: Omit<ChapterCardProps, 'chapter'> & {
index: number;
isSortDesc: boolean;
chapters: ChapterCardProps['chapter'][];
}) => {
const previousChapterIndex = isSortDesc ? index + 1 : index - 1;
const chapter = chapters[index];
const previousChapter = chapters[previousChapterIndex] ?? { chapterNumber: 0 };
const missingChaptersGap = Chapters.getGap(chapter, previousChapter);
const areChaptersMissing = missingChaptersGap > 0;
return (
<Stack sx={applyStyles(!isSortDesc, { flexDirection: 'column-reverse' })}>
<ChapterCard {...chapterCardProps} chapter={chapters[index]} />
{areChaptersMissing && <MissingChaptersInfoSeparator missingChaptersGap={missingChaptersGap} />}
</Stack>
);
};

View File

@@ -407,4 +407,42 @@ export class Chapters {
T[]
>;
}
static getMissingCount<Chapter extends ChapterNumberInfo>(chapters: Chapter[]): number {
const normalizedChapterNumbers = chapters
.map((chapter) => chapter.chapterNumber)
.filter((chapterNumber) => chapterNumber !== -1)
.map(Math.floor)
.toSorted((a, b) => a - b);
const uniqueChapterNumbers = [...new Set(normalizedChapterNumbers)];
return uniqueChapterNumbers.reduce((missingChapterCount, chapterNumber, index, chapterNumbers) => {
const previousChapterNumber = chapterNumbers[index - 1] ?? chapterNumber;
if (chapterNumber > previousChapterNumber + 1) {
return missingChapterCount + (chapterNumber - previousChapterNumber - 1);
}
return missingChapterCount;
}, 0);
}
static getGap<Chapter extends ChapterNumberInfo>(
chapterA: Chapter | undefined,
chapterB: Chapter | undefined,
): number {
if (!chapterA || !chapterB) {
return 0;
}
if (chapterA.chapterNumber === -1 || chapterB.chapterNumber === -1) {
return 0;
}
const higherChapterNumber = Math.max(chapterA.chapterNumber, chapterB.chapterNumber);
const lowerChapterNumber = Math.min(chapterA.chapterNumber, chapterB.chapterNumber);
return higherChapterNumber - lowerChapterNumber - 1;
}
}

View File

@@ -8,8 +8,8 @@
import { Virtuoso, VirtuosoProps } from 'react-virtuoso';
import { useMemo } from 'react';
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
import { ChapterListCard } from '@/modules/chapter/components/cards/ChapterListCard.tsx';
const onSelectNoop = () => {};
@@ -33,10 +33,11 @@ export const ReaderChapterList = ({
totalCount={chapters.length}
computeItemKey={(index) => chapters[index].id}
itemContent={(index) => (
<ChapterCard
key={chapters[index].id}
<ChapterListCard
index={index}
chapters={chapters}
isSortDesc
mode="reader"
chapter={chapters[index]}
showChapterNumber={false}
selected={null}
onSelect={onSelectNoop}

View File

@@ -295,10 +295,7 @@ export class ReaderControls {
return;
}
const missingChapters = Math.max(
0,
Math.trunc(currentChapter.chapterNumber) - Math.trunc(chapterToOpen.chapterNumber) - 1,
);
const missingChapters = Chapters.getGap(currentChapter, chapterToOpen);
const isSameScanlator =
!shouldInformAboutScanlatorChange || currentChapter.scanlator === chapterToOpen.scanlator;
const isContinuousChapter = !shouldInformAboutMissingChapter || !missingChapters;