@@ -139,6 +139,8 @@
|
|||||||
"no_matches": "No chapters matching filter"
|
"no_matches": "No chapters matching filter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"missing_one": "Missing {{count}} chapter",
|
||||||
|
"missing_other": "Missing {{count}} chapters",
|
||||||
"option": {
|
"option": {
|
||||||
"display": {
|
"display": {
|
||||||
"label": {
|
"label": {
|
||||||
@@ -152,6 +154,8 @@
|
|||||||
"downloaded": "Downloaded"
|
"downloaded": "Downloaded"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"value_one": "{{count}} chapter",
|
||||||
|
"value_other": "{{count}} chapters",
|
||||||
"title_one": "Chapter",
|
"title_one": "Chapter",
|
||||||
"title_other": "Chapters"
|
"title_other": "Chapters"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { ComponentProps, useCallback, useMemo, useState } from 'react';
|
|||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
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 { ResumeFab } from '@/modules/manga/components/ResumeFAB.tsx';
|
||||||
import {
|
import {
|
||||||
filterAndSortChapters,
|
filterAndSortChapters,
|
||||||
@@ -45,6 +44,7 @@ import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
|||||||
import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts';
|
import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts';
|
||||||
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
||||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||||
|
import { ChapterListCard } from '@/modules/chapter/components/cards/ChapterListCard.tsx';
|
||||||
|
|
||||||
type ChapterListHeaderProps = {
|
type ChapterListHeaderProps = {
|
||||||
scrollbarWidth: number;
|
scrollbarWidth: number;
|
||||||
@@ -145,6 +145,7 @@ export const ChapterList = ({
|
|||||||
const visibleChapterIds = useMemo(() => Chapters.getIds(visibleChapters), [visibleChapters]);
|
const visibleChapterIds = useMemo(() => Chapters.getIds(visibleChapters), [visibleChapters]);
|
||||||
const areAllChaptersRead = Mangas.isFullyRead(manga);
|
const areAllChaptersRead = Mangas.isFullyRead(manga);
|
||||||
const areAllChaptersDownloaded = Mangas.isFullyDownloaded(manga);
|
const areAllChaptersDownloaded = Mangas.isFullyDownloaded(manga);
|
||||||
|
const missingChapterCount = useMemo(() => Chapters.getMissingCount(visibleChapters), [visibleChapters]);
|
||||||
|
|
||||||
const noChaptersFound = chapters.length === 0;
|
const noChaptersFound = chapters.length === 0;
|
||||||
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
|
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
|
||||||
@@ -193,11 +194,18 @@ export const ChapterList = ({
|
|||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
scrollbarWidth={scrollbarWidth}
|
scrollbarWidth={scrollbarWidth}
|
||||||
>
|
>
|
||||||
|
<Stack>
|
||||||
<Typography variant="h5" component="h3">
|
<Typography variant="h5" component="h3">
|
||||||
{`${visibleChapters.length} ${t('chapter.title_one', {
|
{t('chapter.value', { count: visibleChapters.length })}
|
||||||
count: visibleChapters.length,
|
</Typography>
|
||||||
|
{!!missingChapterCount && (
|
||||||
|
<Typography variant="body2" color="warning">
|
||||||
|
{`${t('chapter.missing', {
|
||||||
|
count: missingChapterCount,
|
||||||
})}`}
|
})}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Stack direction="row">
|
<Stack direction="row">
|
||||||
{areNoItemsSelected && (
|
{areNoItemsSelected && (
|
||||||
@@ -235,8 +243,10 @@ export const ChapterList = ({
|
|||||||
totalCount={visibleChapters.length}
|
totalCount={visibleChapters.length}
|
||||||
computeItemKey={(index) => visibleChapters[index].id}
|
computeItemKey={(index) => visibleChapters[index].id}
|
||||||
itemContent={(index: number) => (
|
itemContent={(index: number) => (
|
||||||
<ChapterCard
|
<ChapterListCard
|
||||||
chapter={visibleChapters[index]}
|
index={index}
|
||||||
|
isSortDesc={options.reverse}
|
||||||
|
chapters={visibleChapters}
|
||||||
selected={!areNoItemsSelected ? selectedItemIds.includes(visibleChapters[index].id) : null}
|
selected={!areNoItemsSelected ? selectedItemIds.includes(visibleChapters[index].id) : null}
|
||||||
showChapterNumber={options.showChapterNumber}
|
showChapterNumber={options.showChapterNumber}
|
||||||
onSelect={onSelect}
|
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[]
|
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 { Virtuoso, VirtuosoProps } from 'react-virtuoso';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
|
|
||||||
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||||
|
import { ChapterListCard } from '@/modules/chapter/components/cards/ChapterListCard.tsx';
|
||||||
|
|
||||||
const onSelectNoop = () => {};
|
const onSelectNoop = () => {};
|
||||||
|
|
||||||
@@ -33,10 +33,11 @@ export const ReaderChapterList = ({
|
|||||||
totalCount={chapters.length}
|
totalCount={chapters.length}
|
||||||
computeItemKey={(index) => chapters[index].id}
|
computeItemKey={(index) => chapters[index].id}
|
||||||
itemContent={(index) => (
|
itemContent={(index) => (
|
||||||
<ChapterCard
|
<ChapterListCard
|
||||||
key={chapters[index].id}
|
index={index}
|
||||||
|
chapters={chapters}
|
||||||
|
isSortDesc
|
||||||
mode="reader"
|
mode="reader"
|
||||||
chapter={chapters[index]}
|
|
||||||
showChapterNumber={false}
|
showChapterNumber={false}
|
||||||
selected={null}
|
selected={null}
|
||||||
onSelect={onSelectNoop}
|
onSelect={onSelectNoop}
|
||||||
|
|||||||
@@ -295,10 +295,7 @@ export class ReaderControls {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const missingChapters = Math.max(
|
const missingChapters = Chapters.getGap(currentChapter, chapterToOpen);
|
||||||
0,
|
|
||||||
Math.trunc(currentChapter.chapterNumber) - Math.trunc(chapterToOpen.chapterNumber) - 1,
|
|
||||||
);
|
|
||||||
const isSameScanlator =
|
const isSameScanlator =
|
||||||
!shouldInformAboutScanlatorChange || currentChapter.scanlator === chapterToOpen.scanlator;
|
!shouldInformAboutScanlatorChange || currentChapter.scanlator === chapterToOpen.scanlator;
|
||||||
const isContinuousChapter = !shouldInformAboutMissingChapter || !missingChapters;
|
const isContinuousChapter = !shouldInformAboutMissingChapter || !missingChapters;
|
||||||
|
|||||||
Reference in New Issue
Block a user