From 61471ae214dd3e68a41bf76083d201d32c73baa8 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 24 May 2025 20:32:03 +0200 Subject: [PATCH] Show missing chapter info in chapter list Closes #836 --- public/locales/en.json | 4 ++ .../chapter/components/ChapterList.tsx | 26 ++++++--- .../MissingChaptersInfoSeparator.tsx | 55 +++++++++++++++++++ .../components/cards/ChapterListCard.tsx | 43 +++++++++++++++ src/modules/chapter/services/Chapters.ts | 38 +++++++++++++ .../overlay/navigation/ReaderChapterList.tsx | 9 +-- src/modules/reader/services/ReaderControls.ts | 5 +- 7 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 src/modules/chapter/components/MissingChaptersInfoSeparator.tsx create mode 100644 src/modules/chapter/components/cards/ChapterListCard.tsx 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) => ( -