@@ -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"
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<Stack>
|
||||
<Typography variant="h5" component="h3">
|
||||
{`${visibleChapters.length} ${t('chapter.title_one', {
|
||||
count: visibleChapters.length,
|
||||
{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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
43
src/modules/chapter/components/cards/ChapterListCard.tsx
Normal file
43
src/modules/chapter/components/cards/ChapterListCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user