Move download status subscription to "ChapterCard"
Only re-render chapters whose download status has changed instead of the whole list everytime a download status change was received
This commit is contained in:
@@ -28,6 +28,7 @@ import { ChaptersToolbarMenu } from '@/modules/chapter/components/ChaptersToolba
|
||||
import { SelectionFAB } from '@/modules/collection/components/SelectionFAB.tsx';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
|
||||
import {
|
||||
ChapterListFieldsFragment,
|
||||
GetChaptersMangaQuery,
|
||||
GetChaptersMangaQueryVariables,
|
||||
MangaScreenFieldsFragment,
|
||||
@@ -35,7 +36,6 @@ import {
|
||||
import { useSelectableCollection } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
||||
import { SelectableCollectionSelectAll } from '@/modules/collection/components/SelectableCollectionSelectAll.tsx';
|
||||
import { Chapters } from '@/modules/chapter/services/Chapters.ts';
|
||||
import { ChaptersWithMeta, ChapterWithMetaType } from '@/modules/chapter/services/ChaptersWithMeta.ts';
|
||||
import { ChapterActionMenuItems } from '@/modules/chapter/components/actions/ChapterActionMenuItems.tsx';
|
||||
import { ChaptersDownloadActionMenuItems } from '@/modules/chapter/components/actions/ChaptersDownloadActionMenuItems.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
@@ -75,9 +75,27 @@ const StyledVirtuoso = styled(Virtuoso, {
|
||||
},
|
||||
}));
|
||||
|
||||
export interface IChapterWithMeta extends ChapterWithMetaType<ComponentProps<typeof ChapterCard>['chapter']> {
|
||||
selected: boolean | null;
|
||||
}
|
||||
const ChapterListFAB = ({
|
||||
selectedChapters,
|
||||
firstUnreadChapter,
|
||||
}: {
|
||||
selectedChapters: ChapterListFieldsFragment[];
|
||||
firstUnreadChapter: ComponentProps<typeof ResumeFab>['chapter'] | null | undefined;
|
||||
}) => {
|
||||
if (selectedChapters.length) {
|
||||
return (
|
||||
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title_one">
|
||||
{(handleClose) => <ChapterActionMenuItems selectedChapters={selectedChapters} onClose={handleClose} />}
|
||||
</SelectionFAB>
|
||||
);
|
||||
}
|
||||
|
||||
if (firstUnreadChapter) {
|
||||
return <ResumeFab chapter={firstUnreadChapter} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const ChapterList = ({
|
||||
manga,
|
||||
@@ -100,8 +118,6 @@ export const ChapterList = ({
|
||||
|
||||
const scrollbarWidth = MediaQuery.useGetScrollbarSize('width');
|
||||
|
||||
const downloadSubscription = requestManager.useDownloadSubscription();
|
||||
|
||||
const [options, dispatch] = useChapterOptions(manga.id);
|
||||
const {
|
||||
data: chaptersData,
|
||||
@@ -128,39 +144,6 @@ export const ChapterList = ({
|
||||
const noChaptersFound = chapters.length === 0;
|
||||
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
|
||||
|
||||
const chaptersWithMeta: IChapterWithMeta[] = useMemo(
|
||||
() =>
|
||||
visibleChapters.map((chapter) => {
|
||||
const selected = !areNoItemsSelected ? selectedItemIds.includes(chapter.id) : null;
|
||||
return {
|
||||
chapter,
|
||||
downloadChapter: Chapters.getDownloadStatusFromCache(chapter.id),
|
||||
selected,
|
||||
};
|
||||
}),
|
||||
[downloadSubscription.data?.downloadStatusChanged, selectedItemIds, visibleChapters],
|
||||
);
|
||||
|
||||
const chapterListFAB = useMemo(() => {
|
||||
const selectedChapters = chaptersWithMeta.filter((chapter) => chapter.selected);
|
||||
|
||||
if (selectedChapters.length) {
|
||||
return (
|
||||
<SelectionFAB selectedItemsCount={selectedChapters.length} title="chapter.title_one">
|
||||
{(handleClose) => (
|
||||
<ChapterActionMenuItems selectedChapters={selectedChapters} onClose={handleClose} />
|
||||
)}
|
||||
</SelectionFAB>
|
||||
);
|
||||
}
|
||||
|
||||
if (manga.firstUnreadChapter) {
|
||||
return <ResumeFab chapter={manga.firstUnreadChapter} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [chaptersWithMeta]);
|
||||
|
||||
if (isLoading || (noChaptersFound && isRefreshing)) {
|
||||
return (
|
||||
<Stack sx={{ justifyContent: 'center', alignItems: 'center', position: 'relative', flexGrow: 1 }}>
|
||||
@@ -201,13 +184,7 @@ export const ChapterList = ({
|
||||
<Tooltip title={t('chapter.action.mark_as_read.add.label.action.current')}>
|
||||
<IconButton
|
||||
disabled={areAllChaptersRead}
|
||||
onClick={() =>
|
||||
Chapters.markAsRead(
|
||||
ChaptersWithMeta.getChapters(ChaptersWithMeta.getNonRead(chaptersWithMeta)),
|
||||
true,
|
||||
manga.id,
|
||||
)
|
||||
}
|
||||
onClick={() => Chapters.markAsRead(Chapters.getNonRead(chapters), true, manga.id)}
|
||||
>
|
||||
<DoneAllIcon />
|
||||
</IconButton>
|
||||
@@ -259,11 +236,12 @@ export const ChapterList = ({
|
||||
computeItemKey={(index) => visibleChapters[index].id}
|
||||
itemContent={(index: number) => (
|
||||
<ChapterCard
|
||||
{...chaptersWithMeta[index]}
|
||||
chapter={chapters[index]}
|
||||
selected={!areNoItemsSelected ? selectedItemIds.includes(chapters[index].id) : null}
|
||||
allChapters={chapters}
|
||||
showChapterNumber={options.showChapterNumber}
|
||||
onSelect={(selected, selectRange) =>
|
||||
handleSelection(chaptersWithMeta[index].chapter.id, selected, { selectRange })
|
||||
handleSelection(chapters[index].id, selected, { selectRange })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -271,7 +249,12 @@ export const ChapterList = ({
|
||||
overscan={window.innerHeight * 0.5}
|
||||
/>
|
||||
</Stack>
|
||||
{chapterListFAB}
|
||||
<ChapterListFAB
|
||||
selectedChapters={selectedItemIds
|
||||
.map((id) => chapters.find((chapter) => chapter.id === id))
|
||||
.filter((chapter) => chapter != null)}
|
||||
firstUnreadChapter={manga.firstUnreadChapter}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import BookmarkRemove from '@mui/icons-material/BookmarkRemove';
|
||||
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
|
||||
import DoneAll from '@mui/icons-material/DoneAll';
|
||||
import { useMemo } from 'react';
|
||||
import { ComponentProps, useMemo } from 'react';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
||||
import {
|
||||
@@ -30,8 +30,6 @@ import {
|
||||
Chapters,
|
||||
} from '@/modules/chapter/services/Chapters.ts';
|
||||
import { MenuItem } from '@/modules/core/components/menu/MenuItem.tsx';
|
||||
import { IChapterWithMeta } from '@/modules/chapter/components/ChapterList.tsx';
|
||||
import { ChaptersWithMeta } from '@/modules/chapter/services/ChaptersWithMeta.ts';
|
||||
import {
|
||||
createGetMenuItemTitle,
|
||||
createIsMenuItemDisabled,
|
||||
@@ -39,6 +37,7 @@ import {
|
||||
} from '@/modules/core/components/menu/Menu.utils.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
||||
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
|
||||
|
||||
type BaseProps = { onClose: () => void; selectable?: boolean };
|
||||
|
||||
@@ -57,7 +56,7 @@ type SingleModeProps = {
|
||||
};
|
||||
|
||||
type SelectModeProps = {
|
||||
selectedChapters: IChapterWithMeta[];
|
||||
selectedChapters: ComponentProps<typeof ChapterCard>['chapter'][];
|
||||
};
|
||||
|
||||
type Props =
|
||||
@@ -95,12 +94,12 @@ export const ChapterActionMenuItems = ({
|
||||
readChapters,
|
||||
} = useMemo(
|
||||
() => ({
|
||||
downloadableChapters: ChaptersWithMeta.getDownloadable(selectedChapters),
|
||||
downloadedChapters: ChaptersWithMeta.getDownloaded(selectedChapters),
|
||||
unbookmarkedChapters: ChaptersWithMeta.getNonBookmarked(selectedChapters),
|
||||
bookmarkedChapters: ChaptersWithMeta.getBookmarked(selectedChapters),
|
||||
unreadChapters: ChaptersWithMeta.getNonRead(selectedChapters),
|
||||
readChapters: ChaptersWithMeta.getRead(selectedChapters),
|
||||
downloadableChapters: Chapters.getDownloadable(selectedChapters),
|
||||
downloadedChapters: Chapters.getDownloaded(selectedChapters),
|
||||
unbookmarkedChapters: Chapters.getNonBookmarked(selectedChapters),
|
||||
bookmarkedChapters: Chapters.getBookmarked(selectedChapters),
|
||||
unreadChapters: Chapters.getNonRead(selectedChapters),
|
||||
readChapters: Chapters.getRead(selectedChapters),
|
||||
}),
|
||||
[selectedChapters],
|
||||
);
|
||||
@@ -110,7 +109,7 @@ export const ChapterActionMenuItems = ({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const performAction = (action: ChapterAction | 'mark_prev_as_read', chaptersWithMeta: IChapterWithMeta[]) => {
|
||||
const performAction = (action: ChapterAction | 'mark_prev_as_read', chapters: TChapter[]) => {
|
||||
const isMarkPrevAsRead = action === 'mark_prev_as_read';
|
||||
const actualAction: ChapterAction = isMarkPrevAsRead ? 'mark_as_read' : action;
|
||||
|
||||
@@ -125,7 +124,7 @@ export const ChapterActionMenuItems = ({
|
||||
const getChapters = (): SingleModeProps['chapter'][] => {
|
||||
// select mode
|
||||
if (!chapter) {
|
||||
return ChaptersWithMeta.getChapters(chaptersWithMeta);
|
||||
return chapters;
|
||||
}
|
||||
|
||||
if (!isMarkPrevAsRead) {
|
||||
@@ -142,17 +141,17 @@ export const ChapterActionMenuItems = ({
|
||||
return allChapters.slice(index + 1);
|
||||
};
|
||||
|
||||
const chapters = getChapters();
|
||||
const chaptersToUpdate = getChapters();
|
||||
|
||||
if (!chapters.length) {
|
||||
if (!chaptersToUpdate.length) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
Chapters.performAction(actualAction, Chapters.getIds(chapters), {
|
||||
chapters,
|
||||
Chapters.performAction(actualAction, Chapters.getIds(chaptersToUpdate), {
|
||||
chapters: chaptersToUpdate,
|
||||
wasManuallyMarkedAsRead: true,
|
||||
trackProgressMangaId: chapters[0]?.mangaId,
|
||||
trackProgressMangaId: chaptersToUpdate[0]?.mangaId,
|
||||
}).catch(defaultPromiseErrorHandler('ChapterActionMenuItems::performAction'));
|
||||
onClose();
|
||||
};
|
||||
@@ -186,10 +185,7 @@ export const ChapterActionMenuItems = ({
|
||||
Icon={Delete}
|
||||
disabled={isMenuItemDisabled(!downloadedChapters.length)}
|
||||
onClick={() =>
|
||||
performAction(
|
||||
'delete',
|
||||
ChaptersWithMeta.getDeletable(downloadedChapters, deleteChaptersWithBookmark),
|
||||
)
|
||||
performAction('delete', Chapters.getDeletable(downloadedChapters, deleteChaptersWithBookmark))
|
||||
}
|
||||
title={getMenuItemTitle('delete', downloadedChapters.length)}
|
||||
/>
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
Chapters,
|
||||
ChapterScanlatorInfo,
|
||||
} from '@/modules/chapter/services/Chapters.ts';
|
||||
import { ChaptersWithMeta, ChapterWithMetaType } from '@/modules/chapter/services/ChaptersWithMeta.ts';
|
||||
|
||||
type TChapter = ChapterIdInfo &
|
||||
ChapterMangaInfo &
|
||||
@@ -53,7 +52,6 @@ interface IProps {
|
||||
mode?: 'manga.page' | 'reader';
|
||||
chapter: TChapter;
|
||||
allChapters: TChapter[];
|
||||
downloadChapter: ChapterWithMetaType['downloadChapter'];
|
||||
showChapterNumber: boolean;
|
||||
onSelect: (selected: boolean, isShiftKey?: boolean) => void;
|
||||
selected: boolean | null;
|
||||
@@ -71,7 +69,6 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
mode = 'manga.page',
|
||||
chapter,
|
||||
allChapters,
|
||||
downloadChapter: dc,
|
||||
showChapterNumber,
|
||||
onSelect,
|
||||
selected,
|
||||
@@ -175,7 +172,7 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{dc && <DownloadStateIndicator download={dc} />}
|
||||
<DownloadStateIndicator chapterId={chapter.id} />
|
||||
|
||||
<Stack sx={{ minHeight: '48px' }}>
|
||||
{selected === null ? (
|
||||
@@ -209,10 +206,7 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
chapter={chapter}
|
||||
allChapters={allChapters}
|
||||
handleSelection={() => onSelect(true)}
|
||||
canBeDownloaded={ChaptersWithMeta.isDownloadable({
|
||||
chapter,
|
||||
downloadChapter: dc,
|
||||
})}
|
||||
canBeDownloaded={Chapters.isDownloadable(chapter)}
|
||||
selectable={selectable}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
|
||||
import { t as translate } from 'i18next';
|
||||
import gql from 'graphql-tag';
|
||||
import { DocumentNode, Unmasked } from '@apollo/client';
|
||||
import { DocumentNode, MaybeMasked, Unmasked, useFragment } from '@apollo/client';
|
||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { getMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
||||
import {
|
||||
ChapterListFieldsFragment,
|
||||
ChapterType,
|
||||
DownloadState,
|
||||
DownloadStatusFieldsFragment,
|
||||
DownloadTypeFieldsFragment,
|
||||
} from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -138,6 +139,30 @@ export class Chapters {
|
||||
});
|
||||
}
|
||||
|
||||
static useDownloadStatusFromCache<T = DownloadTypeFieldsFragment>(
|
||||
id: number,
|
||||
fragment: DocumentNode = DOWNLOAD_TYPE_FIELDS,
|
||||
fragmentName: string = 'DOWNLOAD_TYPE_FIELDS',
|
||||
): MaybeMasked<T> | null {
|
||||
const downloadStatus = useFragment<T>({
|
||||
from: {
|
||||
__typename: 'DownloadType',
|
||||
chapter: {
|
||||
__ref: requestManager.graphQLClient.client.cache.identify({ __typename: 'ChapterType', id }),
|
||||
},
|
||||
},
|
||||
fragment,
|
||||
fragmentName,
|
||||
client: requestManager.graphQLClient.client,
|
||||
});
|
||||
|
||||
if (!downloadStatus.complete || !Object.keys(downloadStatus.data ?? {}).length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return downloadStatus.data;
|
||||
}
|
||||
|
||||
static getReaderUrl<Chapter extends ChapterMangaInfo & ChapterSourceOrderInfo>(chapter: Chapter): string {
|
||||
return AppRoutes.reader.path(chapter.mangaId, chapter.sourceOrder);
|
||||
}
|
||||
@@ -170,6 +195,15 @@ export class Chapters {
|
||||
return chapters.filter(Chapters.isDownloaded);
|
||||
}
|
||||
|
||||
static isDownloadable<Chapter extends ChapterDownloadInfo>(chapter: Chapter): boolean {
|
||||
const downloadStatus = Chapters.getDownloadStatusFromCache(chapter.id);
|
||||
return !Chapters.isDownloaded(chapter) && (!downloadStatus || downloadStatus.state === DownloadState.Error);
|
||||
}
|
||||
|
||||
static getDownloadable<Chapter extends ChapterDownloadInfo>(chapters: Chapter[]): Chapter[] {
|
||||
return chapters.filter(this.isDownloadable);
|
||||
}
|
||||
|
||||
static isDeletable(
|
||||
{ isBookmarked, ...chapter }: ChapterDownloadInfo & ChapterBookmarkInfo,
|
||||
canDeleteBookmarked: boolean = false,
|
||||
|
||||
@@ -1,76 +0,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 {
|
||||
ChapterBookmarkInfo,
|
||||
ChapterDownloadInfo,
|
||||
ChapterDownloadStatus,
|
||||
ChapterReadInfo,
|
||||
Chapters,
|
||||
} from '@/modules/chapter/services/Chapters.ts';
|
||||
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
export type ChapterWithMetaType<
|
||||
Chapter extends ChapterDownloadInfo & ChapterReadInfo & ChapterBookmarkInfo = ChapterDownloadInfo &
|
||||
ChapterReadInfo &
|
||||
ChapterBookmarkInfo,
|
||||
> = {
|
||||
chapter: Chapter;
|
||||
downloadChapter: ChapterDownloadStatus | undefined | null;
|
||||
};
|
||||
|
||||
export class ChaptersWithMeta {
|
||||
static getChapters<ChaptersWithMeta extends ChapterWithMetaType>(
|
||||
chapters: ChaptersWithMeta[],
|
||||
): ChaptersWithMeta['chapter'][] {
|
||||
return chapters.map(({ chapter }) => chapter);
|
||||
}
|
||||
|
||||
static getIds(chapters: ChapterWithMetaType[]): number[] {
|
||||
return Chapters.getIds(ChaptersWithMeta.getChapters(chapters));
|
||||
}
|
||||
|
||||
static getDownloaded<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||
return chapters.filter(({ chapter }) => Chapters.isDownloaded(chapter));
|
||||
}
|
||||
|
||||
static getDeletable<Chapter extends ChapterWithMetaType>(
|
||||
chapters: Chapter[],
|
||||
canDeleteBookmarked?: boolean,
|
||||
): Chapter[] {
|
||||
return chapters.filter(({ chapter }) => Chapters.isDeletable(chapter, canDeleteBookmarked));
|
||||
}
|
||||
|
||||
static getNonDownloaded<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||
return chapters.filter(({ chapter }) => !Chapters.isDownloaded(chapter));
|
||||
}
|
||||
|
||||
static isDownloadable<Chapter extends ChapterWithMetaType>({ chapter, downloadChapter }: Chapter): boolean {
|
||||
return !Chapters.isDownloaded(chapter) && (!downloadChapter || downloadChapter?.state === DownloadState.Error);
|
||||
}
|
||||
|
||||
static getDownloadable<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||
return chapters.filter(this.isDownloadable);
|
||||
}
|
||||
|
||||
static getBookmarked<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||
return chapters.filter(({ chapter }) => Chapters.isBookmarked(chapter));
|
||||
}
|
||||
|
||||
static getNonBookmarked<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||
return chapters.filter(({ chapter }) => !Chapters.isBookmarked(chapter));
|
||||
}
|
||||
|
||||
static getRead<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||
return chapters.filter(({ chapter }) => Chapters.isRead(chapter));
|
||||
}
|
||||
|
||||
static getNonRead<Chapter extends ChapterWithMetaType>(chapters: Chapter[]): Chapter[] {
|
||||
return chapters.filter(({ chapter }) => !Chapters.isRead(chapter));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user