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 { SelectionFAB } from '@/modules/collection/components/SelectionFAB.tsx';
|
||||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
|
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
|
||||||
import {
|
import {
|
||||||
|
ChapterListFieldsFragment,
|
||||||
GetChaptersMangaQuery,
|
GetChaptersMangaQuery,
|
||||||
GetChaptersMangaQueryVariables,
|
GetChaptersMangaQueryVariables,
|
||||||
MangaScreenFieldsFragment,
|
MangaScreenFieldsFragment,
|
||||||
@@ -35,7 +36,6 @@ import {
|
|||||||
import { useSelectableCollection } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
import { useSelectableCollection } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
||||||
import { SelectableCollectionSelectAll } from '@/modules/collection/components/SelectableCollectionSelectAll.tsx';
|
import { SelectableCollectionSelectAll } from '@/modules/collection/components/SelectableCollectionSelectAll.tsx';
|
||||||
import { Chapters } from '@/modules/chapter/services/Chapters.ts';
|
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 { ChapterActionMenuItems } from '@/modules/chapter/components/actions/ChapterActionMenuItems.tsx';
|
||||||
import { ChaptersDownloadActionMenuItems } from '@/modules/chapter/components/actions/ChaptersDownloadActionMenuItems.tsx';
|
import { ChaptersDownloadActionMenuItems } from '@/modules/chapter/components/actions/ChaptersDownloadActionMenuItems.tsx';
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||||
@@ -75,9 +75,27 @@ const StyledVirtuoso = styled(Virtuoso, {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface IChapterWithMeta extends ChapterWithMetaType<ComponentProps<typeof ChapterCard>['chapter']> {
|
const ChapterListFAB = ({
|
||||||
selected: boolean | null;
|
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 = ({
|
export const ChapterList = ({
|
||||||
manga,
|
manga,
|
||||||
@@ -100,8 +118,6 @@ export const ChapterList = ({
|
|||||||
|
|
||||||
const scrollbarWidth = MediaQuery.useGetScrollbarSize('width');
|
const scrollbarWidth = MediaQuery.useGetScrollbarSize('width');
|
||||||
|
|
||||||
const downloadSubscription = requestManager.useDownloadSubscription();
|
|
||||||
|
|
||||||
const [options, dispatch] = useChapterOptions(manga.id);
|
const [options, dispatch] = useChapterOptions(manga.id);
|
||||||
const {
|
const {
|
||||||
data: chaptersData,
|
data: chaptersData,
|
||||||
@@ -128,39 +144,6 @@ export const ChapterList = ({
|
|||||||
const noChaptersFound = chapters.length === 0;
|
const noChaptersFound = chapters.length === 0;
|
||||||
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.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)) {
|
if (isLoading || (noChaptersFound && isRefreshing)) {
|
||||||
return (
|
return (
|
||||||
<Stack sx={{ justifyContent: 'center', alignItems: 'center', position: 'relative', flexGrow: 1 }}>
|
<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')}>
|
<Tooltip title={t('chapter.action.mark_as_read.add.label.action.current')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={areAllChaptersRead}
|
disabled={areAllChaptersRead}
|
||||||
onClick={() =>
|
onClick={() => Chapters.markAsRead(Chapters.getNonRead(chapters), true, manga.id)}
|
||||||
Chapters.markAsRead(
|
|
||||||
ChaptersWithMeta.getChapters(ChaptersWithMeta.getNonRead(chaptersWithMeta)),
|
|
||||||
true,
|
|
||||||
manga.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<DoneAllIcon />
|
<DoneAllIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -259,11 +236,12 @@ export const ChapterList = ({
|
|||||||
computeItemKey={(index) => visibleChapters[index].id}
|
computeItemKey={(index) => visibleChapters[index].id}
|
||||||
itemContent={(index: number) => (
|
itemContent={(index: number) => (
|
||||||
<ChapterCard
|
<ChapterCard
|
||||||
{...chaptersWithMeta[index]}
|
chapter={chapters[index]}
|
||||||
|
selected={!areNoItemsSelected ? selectedItemIds.includes(chapters[index].id) : null}
|
||||||
allChapters={chapters}
|
allChapters={chapters}
|
||||||
showChapterNumber={options.showChapterNumber}
|
showChapterNumber={options.showChapterNumber}
|
||||||
onSelect={(selected, selectRange) =>
|
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}
|
overscan={window.innerHeight * 0.5}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</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 BookmarkRemove from '@mui/icons-material/BookmarkRemove';
|
||||||
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
|
import BookmarkAdd from '@mui/icons-material/BookmarkAdd';
|
||||||
import DoneAll from '@mui/icons-material/DoneAll';
|
import DoneAll from '@mui/icons-material/DoneAll';
|
||||||
import { useMemo } from 'react';
|
import { ComponentProps, useMemo } from 'react';
|
||||||
import LaunchIcon from '@mui/icons-material/Launch';
|
import LaunchIcon from '@mui/icons-material/Launch';
|
||||||
import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
||||||
import {
|
import {
|
||||||
@@ -30,8 +30,6 @@ import {
|
|||||||
Chapters,
|
Chapters,
|
||||||
} from '@/modules/chapter/services/Chapters.ts';
|
} from '@/modules/chapter/services/Chapters.ts';
|
||||||
import { MenuItem } from '@/modules/core/components/menu/MenuItem.tsx';
|
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 {
|
import {
|
||||||
createGetMenuItemTitle,
|
createGetMenuItemTitle,
|
||||||
createIsMenuItemDisabled,
|
createIsMenuItemDisabled,
|
||||||
@@ -39,6 +37,7 @@ import {
|
|||||||
} from '@/modules/core/components/menu/Menu.utils.ts';
|
} from '@/modules/core/components/menu/Menu.utils.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||||
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
||||||
|
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
|
||||||
|
|
||||||
type BaseProps = { onClose: () => void; selectable?: boolean };
|
type BaseProps = { onClose: () => void; selectable?: boolean };
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ type SingleModeProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type SelectModeProps = {
|
type SelectModeProps = {
|
||||||
selectedChapters: IChapterWithMeta[];
|
selectedChapters: ComponentProps<typeof ChapterCard>['chapter'][];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props =
|
type Props =
|
||||||
@@ -95,12 +94,12 @@ export const ChapterActionMenuItems = ({
|
|||||||
readChapters,
|
readChapters,
|
||||||
} = useMemo(
|
} = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
downloadableChapters: ChaptersWithMeta.getDownloadable(selectedChapters),
|
downloadableChapters: Chapters.getDownloadable(selectedChapters),
|
||||||
downloadedChapters: ChaptersWithMeta.getDownloaded(selectedChapters),
|
downloadedChapters: Chapters.getDownloaded(selectedChapters),
|
||||||
unbookmarkedChapters: ChaptersWithMeta.getNonBookmarked(selectedChapters),
|
unbookmarkedChapters: Chapters.getNonBookmarked(selectedChapters),
|
||||||
bookmarkedChapters: ChaptersWithMeta.getBookmarked(selectedChapters),
|
bookmarkedChapters: Chapters.getBookmarked(selectedChapters),
|
||||||
unreadChapters: ChaptersWithMeta.getNonRead(selectedChapters),
|
unreadChapters: Chapters.getNonRead(selectedChapters),
|
||||||
readChapters: ChaptersWithMeta.getRead(selectedChapters),
|
readChapters: Chapters.getRead(selectedChapters),
|
||||||
}),
|
}),
|
||||||
[selectedChapters],
|
[selectedChapters],
|
||||||
);
|
);
|
||||||
@@ -110,7 +109,7 @@ export const ChapterActionMenuItems = ({
|
|||||||
onClose();
|
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 isMarkPrevAsRead = action === 'mark_prev_as_read';
|
||||||
const actualAction: ChapterAction = isMarkPrevAsRead ? 'mark_as_read' : action;
|
const actualAction: ChapterAction = isMarkPrevAsRead ? 'mark_as_read' : action;
|
||||||
|
|
||||||
@@ -125,7 +124,7 @@ export const ChapterActionMenuItems = ({
|
|||||||
const getChapters = (): SingleModeProps['chapter'][] => {
|
const getChapters = (): SingleModeProps['chapter'][] => {
|
||||||
// select mode
|
// select mode
|
||||||
if (!chapter) {
|
if (!chapter) {
|
||||||
return ChaptersWithMeta.getChapters(chaptersWithMeta);
|
return chapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMarkPrevAsRead) {
|
if (!isMarkPrevAsRead) {
|
||||||
@@ -142,17 +141,17 @@ export const ChapterActionMenuItems = ({
|
|||||||
return allChapters.slice(index + 1);
|
return allChapters.slice(index + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const chapters = getChapters();
|
const chaptersToUpdate = getChapters();
|
||||||
|
|
||||||
if (!chapters.length) {
|
if (!chaptersToUpdate.length) {
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Chapters.performAction(actualAction, Chapters.getIds(chapters), {
|
Chapters.performAction(actualAction, Chapters.getIds(chaptersToUpdate), {
|
||||||
chapters,
|
chapters: chaptersToUpdate,
|
||||||
wasManuallyMarkedAsRead: true,
|
wasManuallyMarkedAsRead: true,
|
||||||
trackProgressMangaId: chapters[0]?.mangaId,
|
trackProgressMangaId: chaptersToUpdate[0]?.mangaId,
|
||||||
}).catch(defaultPromiseErrorHandler('ChapterActionMenuItems::performAction'));
|
}).catch(defaultPromiseErrorHandler('ChapterActionMenuItems::performAction'));
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -186,10 +185,7 @@ export const ChapterActionMenuItems = ({
|
|||||||
Icon={Delete}
|
Icon={Delete}
|
||||||
disabled={isMenuItemDisabled(!downloadedChapters.length)}
|
disabled={isMenuItemDisabled(!downloadedChapters.length)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
performAction(
|
performAction('delete', Chapters.getDeletable(downloadedChapters, deleteChaptersWithBookmark))
|
||||||
'delete',
|
|
||||||
ChaptersWithMeta.getDeletable(downloadedChapters, deleteChaptersWithBookmark),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
title={getMenuItemTitle('delete', downloadedChapters.length)}
|
title={getMenuItemTitle('delete', downloadedChapters.length)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import {
|
|||||||
Chapters,
|
Chapters,
|
||||||
ChapterScanlatorInfo,
|
ChapterScanlatorInfo,
|
||||||
} from '@/modules/chapter/services/Chapters.ts';
|
} from '@/modules/chapter/services/Chapters.ts';
|
||||||
import { ChaptersWithMeta, ChapterWithMetaType } from '@/modules/chapter/services/ChaptersWithMeta.ts';
|
|
||||||
|
|
||||||
type TChapter = ChapterIdInfo &
|
type TChapter = ChapterIdInfo &
|
||||||
ChapterMangaInfo &
|
ChapterMangaInfo &
|
||||||
@@ -53,7 +52,6 @@ interface IProps {
|
|||||||
mode?: 'manga.page' | 'reader';
|
mode?: 'manga.page' | 'reader';
|
||||||
chapter: TChapter;
|
chapter: TChapter;
|
||||||
allChapters: TChapter[];
|
allChapters: TChapter[];
|
||||||
downloadChapter: ChapterWithMetaType['downloadChapter'];
|
|
||||||
showChapterNumber: boolean;
|
showChapterNumber: boolean;
|
||||||
onSelect: (selected: boolean, isShiftKey?: boolean) => void;
|
onSelect: (selected: boolean, isShiftKey?: boolean) => void;
|
||||||
selected: boolean | null;
|
selected: boolean | null;
|
||||||
@@ -71,7 +69,6 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
mode = 'manga.page',
|
mode = 'manga.page',
|
||||||
chapter,
|
chapter,
|
||||||
allChapters,
|
allChapters,
|
||||||
downloadChapter: dc,
|
|
||||||
showChapterNumber,
|
showChapterNumber,
|
||||||
onSelect,
|
onSelect,
|
||||||
selected,
|
selected,
|
||||||
@@ -175,7 +172,7 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{dc && <DownloadStateIndicator download={dc} />}
|
<DownloadStateIndicator chapterId={chapter.id} />
|
||||||
|
|
||||||
<Stack sx={{ minHeight: '48px' }}>
|
<Stack sx={{ minHeight: '48px' }}>
|
||||||
{selected === null ? (
|
{selected === null ? (
|
||||||
@@ -209,10 +206,7 @@ export const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
|||||||
chapter={chapter}
|
chapter={chapter}
|
||||||
allChapters={allChapters}
|
allChapters={allChapters}
|
||||||
handleSelection={() => onSelect(true)}
|
handleSelection={() => onSelect(true)}
|
||||||
canBeDownloaded={ChaptersWithMeta.isDownloadable({
|
canBeDownloaded={Chapters.isDownloadable(chapter)}
|
||||||
chapter,
|
|
||||||
downloadChapter: dc,
|
|
||||||
})}
|
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,13 +8,14 @@
|
|||||||
|
|
||||||
import { t as translate } from 'i18next';
|
import { t as translate } from 'i18next';
|
||||||
import gql from 'graphql-tag';
|
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 { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { getMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
import { getMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
||||||
import {
|
import {
|
||||||
ChapterListFieldsFragment,
|
ChapterListFieldsFragment,
|
||||||
ChapterType,
|
ChapterType,
|
||||||
|
DownloadState,
|
||||||
DownloadStatusFieldsFragment,
|
DownloadStatusFieldsFragment,
|
||||||
DownloadTypeFieldsFragment,
|
DownloadTypeFieldsFragment,
|
||||||
} from '@/lib/graphql/generated/graphql.ts';
|
} 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 {
|
static getReaderUrl<Chapter extends ChapterMangaInfo & ChapterSourceOrderInfo>(chapter: Chapter): string {
|
||||||
return AppRoutes.reader.path(chapter.mangaId, chapter.sourceOrder);
|
return AppRoutes.reader.path(chapter.mangaId, chapter.sourceOrder);
|
||||||
}
|
}
|
||||||
@@ -170,6 +195,15 @@ export class Chapters {
|
|||||||
return chapters.filter(Chapters.isDownloaded);
|
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(
|
static isDeletable(
|
||||||
{ isBookmarked, ...chapter }: ChapterDownloadInfo & ChapterBookmarkInfo,
|
{ isBookmarked, ...chapter }: ChapterDownloadInfo & ChapterBookmarkInfo,
|
||||||
canDeleteBookmarked: boolean = false,
|
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ import Box from '@mui/material/Box';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
|
import { DownloadState } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { ChapterDownloadStatus } from '@/modules/chapter/services/Chapters.ts';
|
import { ChapterIdInfo, Chapters } from '@/modules/chapter/services/Chapters.ts';
|
||||||
import { TranslationKey } from '@/Base.types.ts';
|
import { TranslationKey } from '@/Base.types.ts';
|
||||||
|
|
||||||
const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: TranslationKey } = {
|
const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: TranslationKey } = {
|
||||||
@@ -21,9 +21,15 @@ const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: Transla
|
|||||||
QUEUED: 'download.state.label.queued',
|
QUEUED: 'download.state.label.queued',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const DownloadStateIndicator = ({ download }: { download: ChapterDownloadStatus }) => {
|
export const DownloadStateIndicator = ({ chapterId }: { chapterId: ChapterIdInfo['id'] }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const download = Chapters.useDownloadStatusFromCache(chapterId);
|
||||||
|
|
||||||
|
if (!download) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const isDownloading = download.state === DownloadState.Downloading;
|
const isDownloading = download.state === DownloadState.Downloading;
|
||||||
const isPartiallyDownloaded = download.progress !== 0;
|
const isPartiallyDownloaded = download.progress !== 0;
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const DownloadChapterItem = ({
|
|||||||
{item.chapter.name}
|
{item.chapter.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<DownloadStateIndicator download={item} />
|
<DownloadStateIndicator chapterId={item.chapter.id} />
|
||||||
{item.state === DownloadState.Error && (
|
{item.state === DownloadState.Error && (
|
||||||
<Tooltip title={t('global.button.retry')}>
|
<Tooltip title={t('global.button.retry')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -8,53 +8,33 @@
|
|||||||
|
|
||||||
import { Virtuoso, VirtuosoProps } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoProps } from 'react-virtuoso';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { IChapterWithMeta } from '@/modules/chapter/components/ChapterList.tsx';
|
|
||||||
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
|
import { ChapterCard } from '@/modules/chapter/components/cards/ChapterCard.tsx';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
|
||||||
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||||
import { Chapters } from '@/modules/chapter/services/Chapters';
|
|
||||||
|
|
||||||
export const ReaderChapterList = ({
|
export const ReaderChapterList = ({
|
||||||
currentChapter,
|
currentChapter,
|
||||||
chapters,
|
chapters,
|
||||||
style,
|
style,
|
||||||
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter'> & Pick<VirtuosoProps<any, any>, 'style'>) => {
|
}: Pick<ReaderStateChapters, 'chapters' | 'currentChapter'> & Pick<VirtuosoProps<any, any>, 'style'>) => {
|
||||||
const downloadSubscription = requestManager.useDownloadSubscription();
|
|
||||||
|
|
||||||
const currentChapterIndex = useMemo(
|
const currentChapterIndex = useMemo(
|
||||||
() => currentChapter && chapters.findIndex((chapter) => chapter.id === currentChapter.id),
|
() => currentChapter && chapters.findIndex((chapter) => chapter.id === currentChapter.id),
|
||||||
[currentChapter, chapters],
|
[currentChapter, chapters],
|
||||||
);
|
);
|
||||||
|
|
||||||
const chaptersWithMeta: IChapterWithMeta[] = useMemo(
|
|
||||||
() =>
|
|
||||||
chapters.map((chapter) => {
|
|
||||||
const downloadChapter = Chapters.getDownloadStatusFromCache(chapter.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
chapter,
|
|
||||||
downloadChapter,
|
|
||||||
selected: null,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
[downloadSubscription.data?.downloadStatusChanged, chapters],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
style={{
|
style={{
|
||||||
height: `calc(${chaptersWithMeta.length} * 100px)`,
|
height: `calc(${chapters.length} * 100px)`,
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
initialTopMostItemIndex={currentChapterIndex ?? 0}
|
initialTopMostItemIndex={currentChapterIndex ?? 0}
|
||||||
totalCount={chaptersWithMeta.length}
|
totalCount={chapters.length}
|
||||||
computeItemKey={(index) => chaptersWithMeta[index].chapter.id}
|
computeItemKey={(index) => chapters[index].id}
|
||||||
itemContent={(index) => (
|
itemContent={(index) => (
|
||||||
<ChapterCard
|
<ChapterCard
|
||||||
key={chaptersWithMeta[index].chapter.id}
|
key={chapters[index].id}
|
||||||
mode="reader"
|
mode="reader"
|
||||||
chapter={chaptersWithMeta[index].chapter}
|
chapter={chapters[index]}
|
||||||
downloadChapter={chaptersWithMeta[index].downloadChapter}
|
|
||||||
allChapters={chapters}
|
allChapters={chapters}
|
||||||
showChapterNumber={false}
|
showChapterNumber={false}
|
||||||
selected={null}
|
selected={null}
|
||||||
|
|||||||
@@ -19,20 +19,17 @@ import { memo, useMemo, useRef } from 'react';
|
|||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import { actionToTranslationKey, ChapterAction, Chapters } from '@/modules/chapter/services/Chapters.ts';
|
import { actionToTranslationKey, ChapterAction, Chapters } from '@/modules/chapter/services/Chapters.ts';
|
||||||
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
import { ReaderStateChapters } from '@/modules/reader/types/Reader.types.ts';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
|
||||||
import { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx';
|
import { DownloadStateIndicator } from '@/modules/core/components/DownloadStateIndicator.tsx';
|
||||||
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
import { ReaderStatePages } from '@/modules/reader/types/ReaderProgressBar.types.ts';
|
||||||
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
import { withPropsFrom } from '@/modules/core/hoc/withPropsFrom.tsx';
|
||||||
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
import { useReaderStateChaptersContext } from '@/modules/reader/contexts/state/ReaderStateChaptersContext.tsx';
|
||||||
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
|
import { userReaderStatePagesContext } from '@/modules/reader/contexts/state/ReaderStatePagesContext.tsx';
|
||||||
import { ChapterWithMetaType } from '@/modules/chapter/services/ChaptersWithMeta.ts';
|
|
||||||
|
|
||||||
const DownloadButton = ({
|
const DownloadButton = ({ currentChapter }: Required<Pick<ReaderStateChapters, 'currentChapter'>>) => {
|
||||||
currentChapter,
|
|
||||||
downloadChapter,
|
|
||||||
}: Required<Pick<ReaderStateChapters, 'currentChapter'>> & Pick<ChapterWithMetaType, 'downloadChapter'>) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const downloadChapter = Chapters.getDownloadStatusFromCache(currentChapter?.id ?? -1);
|
||||||
|
|
||||||
if (currentChapter && Chapters.isDownloaded(currentChapter)) {
|
if (currentChapter && Chapters.isDownloaded(currentChapter)) {
|
||||||
return (
|
return (
|
||||||
<Tooltip title={t(actionToTranslationKey.delete.action.single)}>
|
<Tooltip title={t(actionToTranslationKey.delete.action.single)}>
|
||||||
@@ -44,7 +41,7 @@ const DownloadButton = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (downloadChapter) {
|
if (downloadChapter) {
|
||||||
return <DownloadStateIndicator download={downloadChapter} />;
|
return <DownloadStateIndicator chapterId={downloadChapter.chapter.id} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -74,16 +71,6 @@ const BaseReaderNavBarDesktopActions = memo(
|
|||||||
|
|
||||||
const pageRetryKeyPrefix = useRef<number>(0);
|
const pageRetryKeyPrefix = useRef<number>(0);
|
||||||
|
|
||||||
const downloadSubscription = requestManager.useDownloadSubscription();
|
|
||||||
|
|
||||||
const downloadChapter = useMemo(() => {
|
|
||||||
if (!currentChapter) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Chapters.getDownloadStatusFromCache(currentChapter?.id);
|
|
||||||
}, [downloadSubscription.data?.downloadStatusChanged, id]);
|
|
||||||
|
|
||||||
const haveSomePagesFailedToLoad = useMemo(
|
const haveSomePagesFailedToLoad = useMemo(
|
||||||
() => pageLoadStates.some((pageLoadState) => pageLoadState.error),
|
() => pageLoadStates.some((pageLoadState) => pageLoadState.error),
|
||||||
[pageLoadStates],
|
[pageLoadStates],
|
||||||
@@ -118,7 +105,7 @@ const BaseReaderNavBarDesktopActions = memo(
|
|||||||
<ReplayIcon />
|
<ReplayIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DownloadButton currentChapter={currentChapter} downloadChapter={downloadChapter} />
|
<DownloadButton currentChapter={currentChapter} />
|
||||||
<Tooltip title={t('chapter.action.label.open_on_source')}>
|
<Tooltip title={t('chapter.action.label.open_on_source')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={!realUrl}
|
disabled={!realUrl}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { Chapters } from '@/modules/chapter/services/Chapters.ts';
|
|||||||
|
|
||||||
export const ChapterUpdateCard = ({ chapter }: { chapter: ChapterUpdateListFieldsFragment }) => {
|
export const ChapterUpdateCard = ({ chapter }: { chapter: ChapterUpdateListFieldsFragment }) => {
|
||||||
const { manga } = chapter;
|
const { manga } = chapter;
|
||||||
const download = Chapters.getDownloadStatusFromCache(chapter.id);
|
const download = Chapters.useDownloadStatusFromCache(chapter.id);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -112,7 +112,7 @@ export const ChapterUpdateCard = ({ chapter }: { chapter: ChapterUpdateListField
|
|||||||
</TypographyMaxLines>
|
</TypographyMaxLines>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{download && <DownloadStateIndicator download={download} />}
|
<DownloadStateIndicator chapterId={chapter.id} />
|
||||||
{download?.state === DownloadState.Error && (
|
{download?.state === DownloadState.Error && (
|
||||||
<Tooltip title={t('global.button.retry')}>
|
<Tooltip title={t('global.button.retry')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -58,8 +58,6 @@ export const Updates: React.FC = () => {
|
|||||||
const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]);
|
const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]);
|
||||||
const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]);
|
const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]);
|
||||||
|
|
||||||
requestManager.useDownloadSubscription();
|
|
||||||
|
|
||||||
const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey(
|
const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey(
|
||||||
groupCounts,
|
groupCounts,
|
||||||
useCallback((index) => groupedUpdates[index][0], [groupedUpdates]),
|
useCallback((index) => groupedUpdates[index][0], [groupedUpdates]),
|
||||||
|
|||||||
Reference in New Issue
Block a user