diff --git a/public/locales/en.json b/public/locales/en.json
index a09fdd00..5ffebe2b 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -139,6 +139,8 @@
"no_matches": "No chapters matching filter"
}
},
+ "missing_one": "Missing {{count}} chapter",
+ "missing_other": "Missing {{count}} chapters",
"option": {
"display": {
"label": {
@@ -152,6 +154,8 @@
"downloaded": "Downloaded"
}
},
+ "value_one": "{{count}} chapter",
+ "value_other": "{{count}} chapters",
"title_one": "Chapter",
"title_other": "Chapters"
},
diff --git a/src/modules/chapter/components/ChapterList.tsx b/src/modules/chapter/components/ChapterList.tsx
index dd77f97a..0f57a011 100644
--- a/src/modules/chapter/components/ChapterList.tsx
+++ b/src/modules/chapter/components/ChapterList.tsx
@@ -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}
>
-
- {`${visibleChapters.length} ${t('chapter.title_one', {
- count: visibleChapters.length,
- })}`}
-
+
+
+ {t('chapter.value', { count: visibleChapters.length })}
+
+ {!!missingChapterCount && (
+
+ {`${t('chapter.missing', {
+ count: missingChapterCount,
+ })}`}
+
+ )}
+
{areNoItemsSelected && (
@@ -235,8 +243,10 @@ export const ChapterList = ({
totalCount={visibleChapters.length}
computeItemKey={(index) => visibleChapters[index].id}
itemContent={(index: number) => (
- {
+ const { t } = useTranslation();
+
+ return (
+
+ theme.palette.text.secondary,
+ }}
+ />
+
+ {t('chapter.missing', { count: missingChaptersGap })}
+
+ theme.palette.text.secondary,
+ }}
+ />
+
+ );
+};
diff --git a/src/modules/chapter/components/cards/ChapterListCard.tsx b/src/modules/chapter/components/cards/ChapterListCard.tsx
new file mode 100644
index 00000000..979c65e8
--- /dev/null
+++ b/src/modules/chapter/components/cards/ChapterListCard.tsx
@@ -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;
+
+export const ChapterListCard = ({
+ index,
+ isSortDesc,
+ chapters,
+ ...chapterCardProps
+}: Omit & {
+ 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 (
+
+
+ {areChaptersMissing && }
+
+ );
+};
diff --git a/src/modules/chapter/services/Chapters.ts b/src/modules/chapter/services/Chapters.ts
index 8799d388..f24aaf8f 100644
--- a/src/modules/chapter/services/Chapters.ts
+++ b/src/modules/chapter/services/Chapters.ts
@@ -407,4 +407,42 @@ export class Chapters {
T[]
>;
}
+
+ static getMissingCount(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(
+ 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;
+ }
}
diff --git a/src/modules/reader/components/overlay/navigation/ReaderChapterList.tsx b/src/modules/reader/components/overlay/navigation/ReaderChapterList.tsx
index a7789650..2c335559 100644
--- a/src/modules/reader/components/overlay/navigation/ReaderChapterList.tsx
+++ b/src/modules/reader/components/overlay/navigation/ReaderChapterList.tsx
@@ -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) => (
-