diff --git a/src/modules/chapter/components/ChapterList.tsx b/src/modules/chapter/components/ChapterList.tsx index 033eaddd..8ce4f2b0 100644 --- a/src/modules/chapter/components/ChapterList.tsx +++ b/src/modules/chapter/components/ChapterList.tsx @@ -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['chapter']> { - selected: boolean | null; -} +const ChapterListFAB = ({ + selectedChapters, + firstUnreadChapter, +}: { + selectedChapters: ChapterListFieldsFragment[]; + firstUnreadChapter: ComponentProps['chapter'] | null | undefined; +}) => { + if (selectedChapters.length) { + return ( + + {(handleClose) => } + + ); + } + + if (firstUnreadChapter) { + return ; + } + + 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 ( - - {(handleClose) => ( - - )} - - ); - } - - if (manga.firstUnreadChapter) { - return ; - } - - return null; - }, [chaptersWithMeta]); - if (isLoading || (noChaptersFound && isRefreshing)) { return ( @@ -201,13 +184,7 @@ export const ChapterList = ({ - Chapters.markAsRead( - ChaptersWithMeta.getChapters(ChaptersWithMeta.getNonRead(chaptersWithMeta)), - true, - manga.id, - ) - } + onClick={() => Chapters.markAsRead(Chapters.getNonRead(chapters), true, manga.id)} > @@ -259,11 +236,12 @@ export const ChapterList = ({ computeItemKey={(index) => visibleChapters[index].id} itemContent={(index: number) => ( - 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} /> - {chapterListFAB} + chapters.find((chapter) => chapter.id === id)) + .filter((chapter) => chapter != null)} + firstUnreadChapter={manga.firstUnreadChapter} + /> ); }; diff --git a/src/modules/chapter/components/actions/ChapterActionMenuItems.tsx b/src/modules/chapter/components/actions/ChapterActionMenuItems.tsx index 3e72f09d..0d977af0 100644 --- a/src/modules/chapter/components/actions/ChapterActionMenuItems.tsx +++ b/src/modules/chapter/components/actions/ChapterActionMenuItems.tsx @@ -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['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)} /> diff --git a/src/modules/chapter/components/cards/ChapterCard.tsx b/src/modules/chapter/components/cards/ChapterCard.tsx index d15ae6f1..713a2ba9 100644 --- a/src/modules/chapter/components/cards/ChapterCard.tsx +++ b/src/modules/chapter/components/cards/ChapterCard.tsx @@ -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 = (props: IProps) => { mode = 'manga.page', chapter, allChapters, - downloadChapter: dc, showChapterNumber, onSelect, selected, @@ -175,7 +172,7 @@ export const ChapterCard: React.FC = (props: IProps) => { - {dc && } + {selected === null ? ( @@ -209,10 +206,7 @@ export const ChapterCard: React.FC = (props: IProps) => { chapter={chapter} allChapters={allChapters} handleSelection={() => onSelect(true)} - canBeDownloaded={ChaptersWithMeta.isDownloadable({ - chapter, - downloadChapter: dc, - })} + canBeDownloaded={Chapters.isDownloadable(chapter)} selectable={selectable} /> )} diff --git a/src/modules/chapter/services/Chapters.ts b/src/modules/chapter/services/Chapters.ts index df72981b..41b181d7 100644 --- a/src/modules/chapter/services/Chapters.ts +++ b/src/modules/chapter/services/Chapters.ts @@ -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( + id: number, + fragment: DocumentNode = DOWNLOAD_TYPE_FIELDS, + fragmentName: string = 'DOWNLOAD_TYPE_FIELDS', + ): MaybeMasked | null { + const downloadStatus = useFragment({ + 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: 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: Chapter): boolean { + const downloadStatus = Chapters.getDownloadStatusFromCache(chapter.id); + return !Chapters.isDownloaded(chapter) && (!downloadStatus || downloadStatus.state === DownloadState.Error); + } + + static getDownloadable(chapters: Chapter[]): Chapter[] { + return chapters.filter(this.isDownloadable); + } + static isDeletable( { isBookmarked, ...chapter }: ChapterDownloadInfo & ChapterBookmarkInfo, canDeleteBookmarked: boolean = false, diff --git a/src/modules/chapter/services/ChaptersWithMeta.ts b/src/modules/chapter/services/ChaptersWithMeta.ts deleted file mode 100644 index 9d72f2b6..00000000 --- a/src/modules/chapter/services/ChaptersWithMeta.ts +++ /dev/null @@ -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( - chapters: ChaptersWithMeta[], - ): ChaptersWithMeta['chapter'][] { - return chapters.map(({ chapter }) => chapter); - } - - static getIds(chapters: ChapterWithMetaType[]): number[] { - return Chapters.getIds(ChaptersWithMeta.getChapters(chapters)); - } - - static getDownloaded(chapters: Chapter[]): Chapter[] { - return chapters.filter(({ chapter }) => Chapters.isDownloaded(chapter)); - } - - static getDeletable( - chapters: Chapter[], - canDeleteBookmarked?: boolean, - ): Chapter[] { - return chapters.filter(({ chapter }) => Chapters.isDeletable(chapter, canDeleteBookmarked)); - } - - static getNonDownloaded(chapters: Chapter[]): Chapter[] { - return chapters.filter(({ chapter }) => !Chapters.isDownloaded(chapter)); - } - - static isDownloadable({ chapter, downloadChapter }: Chapter): boolean { - return !Chapters.isDownloaded(chapter) && (!downloadChapter || downloadChapter?.state === DownloadState.Error); - } - - static getDownloadable(chapters: Chapter[]): Chapter[] { - return chapters.filter(this.isDownloadable); - } - - static getBookmarked(chapters: Chapter[]): Chapter[] { - return chapters.filter(({ chapter }) => Chapters.isBookmarked(chapter)); - } - - static getNonBookmarked(chapters: Chapter[]): Chapter[] { - return chapters.filter(({ chapter }) => !Chapters.isBookmarked(chapter)); - } - - static getRead(chapters: Chapter[]): Chapter[] { - return chapters.filter(({ chapter }) => Chapters.isRead(chapter)); - } - - static getNonRead(chapters: Chapter[]): Chapter[] { - return chapters.filter(({ chapter }) => !Chapters.isRead(chapter)); - } -} diff --git a/src/modules/core/components/DownloadStateIndicator.tsx b/src/modules/core/components/DownloadStateIndicator.tsx index cabde95a..382edbb7 100644 --- a/src/modules/core/components/DownloadStateIndicator.tsx +++ b/src/modules/core/components/DownloadStateIndicator.tsx @@ -11,7 +11,7 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; 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'; 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', } as const; -export const DownloadStateIndicator = ({ download }: { download: ChapterDownloadStatus }) => { +export const DownloadStateIndicator = ({ chapterId }: { chapterId: ChapterIdInfo['id'] }) => { const { t } = useTranslation(); + const download = Chapters.useDownloadStatusFromCache(chapterId); + + if (!download) { + return null; + } + const isDownloading = download.state === DownloadState.Downloading; const isPartiallyDownloaded = download.progress !== 0; diff --git a/src/modules/downloads/screens/DownloadQueue.tsx b/src/modules/downloads/screens/DownloadQueue.tsx index 1d96c784..6bed1361 100644 --- a/src/modules/downloads/screens/DownloadQueue.tsx +++ b/src/modules/downloads/screens/DownloadQueue.tsx @@ -85,7 +85,7 @@ const DownloadChapterItem = ({ {item.chapter.name} - + {item.state === DownloadState.Error && ( & Pick, 'style'>) => { - const downloadSubscription = requestManager.useDownloadSubscription(); - const currentChapterIndex = useMemo( () => currentChapter && chapters.findIndex((chapter) => chapter.id === currentChapter.id), [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 ( chaptersWithMeta[index].chapter.id} + totalCount={chapters.length} + computeItemKey={(index) => chapters[index].id} itemContent={(index) => ( > & Pick) => { +const DownloadButton = ({ currentChapter }: Required>) => { const { t } = useTranslation(); + const downloadChapter = Chapters.getDownloadStatusFromCache(currentChapter?.id ?? -1); + if (currentChapter && Chapters.isDownloaded(currentChapter)) { return ( @@ -44,7 +41,7 @@ const DownloadButton = ({ } if (downloadChapter) { - return ; + return ; } return ( @@ -74,16 +71,6 @@ const BaseReaderNavBarDesktopActions = memo( const pageRetryKeyPrefix = useRef(0); - const downloadSubscription = requestManager.useDownloadSubscription(); - - const downloadChapter = useMemo(() => { - if (!currentChapter) { - return null; - } - - return Chapters.getDownloadStatusFromCache(currentChapter?.id); - }, [downloadSubscription.data?.downloadStatusChanged, id]); - const haveSomePagesFailedToLoad = useMemo( () => pageLoadStates.some((pageLoadState) => pageLoadState.error), [pageLoadStates], @@ -118,7 +105,7 @@ const BaseReaderNavBarDesktopActions = memo( - + { const { manga } = chapter; - const download = Chapters.getDownloadStatusFromCache(chapter.id); + const download = Chapters.useDownloadStatusFromCache(chapter.id); const { t } = useTranslation(); const location = useLocation(); @@ -112,7 +112,7 @@ export const ChapterUpdateCard = ({ chapter }: { chapter: ChapterUpdateListField - {download && } + {download?.state === DownloadState.Error && ( { const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]); const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]); - requestManager.useDownloadSubscription(); - const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey( groupCounts, useCallback((index) => groupedUpdates[index][0], [groupedUpdates]),