diff --git a/src/components/manga/ChapterCard.tsx b/src/components/manga/ChapterCard.tsx index 072c3caf..1d011a47 100644 --- a/src/components/manga/ChapterCard.tsx +++ b/src/components/manga/ChapterCard.tsx @@ -31,9 +31,11 @@ import { IChapter, IDownloadChapter } from '@/typings'; import requestManager from '@/lib/requests/RequestManager.ts'; import { getUploadDateString } from '@/util/date'; import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator'; +import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; interface IProps { chapter: IChapter; + chapterIds: number[]; triggerChaptersUpdate: () => void; downloadChapter: IDownloadChapter | undefined; showChapterNumber: boolean; @@ -45,7 +47,15 @@ const ChapterCard: React.FC = (props: IProps) => { const { t } = useTranslation(); const theme = useTheme(); - const { chapter, triggerChaptersUpdate, downloadChapter: dc, showChapterNumber, onSelect, selected } = props; + const { + chapter, + chapterIds, + triggerChaptersUpdate, + downloadChapter: dc, + showChapterNumber, + onSelect, + selected, + } = props; const isSelecting = selected !== null; const [anchorEl, setAnchorEl] = React.useState(null); @@ -62,13 +72,20 @@ const ChapterCard: React.FC = (props: IProps) => { setAnchorEl(null); }; - const sendChange = (key: string, value: any) => { + type UpdatePatchInput = UpdateChapterPatchInput & { markPrevRead?: boolean }; + const sendChange = (key: Key, value: UpdatePatchInput[Key]) => { handleClose(); + if (key === 'markPrevRead') { + const index = chapterIds.findIndex((chapterId) => chapterId === chapter.id); + requestManager.updateChapters(chapterIds.slice(index, -1), { isRead: true }); + return; + } + requestManager - .updateChapter(chapter.mangaId, chapter.index, { + .updateChapter(chapter.id, { [key]: value, - lastPageRead: key === 'read' ? 0 : undefined, + lastPageRead: key === 'isRead' ? 0 : undefined, }) .response.then(() => triggerChaptersUpdate()); }; @@ -79,9 +96,7 @@ const ChapterCard: React.FC = (props: IProps) => { }; const deleteChapter = () => { - requestManager - .deleteDownloadedChapter(chapter.mangaId, chapter.index) - .response.then(() => triggerChaptersUpdate()); + requestManager.deleteDownloadedChapter(chapter.id).response.then(() => triggerChaptersUpdate()); handleClose(); }; @@ -177,7 +192,7 @@ const ChapterCard: React.FC = (props: IProps) => { {t('chapter.action.download.add.label.action')} )} - sendChange('bookmarked', !chapter.bookmarked)}> + sendChange('isBookmarked', !chapter.bookmarked)}> {chapter.bookmarked && } {!chapter.bookmarked && } @@ -187,7 +202,7 @@ const ChapterCard: React.FC = (props: IProps) => { {!chapter.bookmarked && t('chapter.action.bookmark.add.label.action')} - sendChange('read', !chapter.read)}> + sendChange('isRead', !chapter.read)}> {chapter.read && } {!chapter.read && } diff --git a/src/components/manga/ChapterList.tsx b/src/components/manga/ChapterList.tsx index 1d76f5a5..21b5b8e4 100644 --- a/src/components/manga/ChapterList.tsx +++ b/src/components/manga/ChapterList.tsx @@ -11,7 +11,7 @@ import Typography from '@mui/material/Typography'; import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { useTranslation } from 'react-i18next'; -import { BatchChaptersChange, IChapter, IDownloadChapter, IQueue, TranslationKey } from '@/typings'; +import { IChapter, IDownloadChapter, IQueue, TranslationKey } from '@/typings'; import requestManager from '@/lib/requests/RequestManager.ts'; import useSubscription from '@/components/library/useSubscription'; import ChapterCard from '@/components/manga/ChapterCard'; @@ -22,6 +22,7 @@ import makeToast from '@/components/util/Toast'; import ChaptersToolbarMenu from '@/components/manga/ChaptersToolbarMenu'; import SelectionFAB from '@/components/manga/SelectionFAB'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab'; +import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({ listStyle: 'none', @@ -87,6 +88,7 @@ const ChapterList: React.FC = ({ mangaId }) => { const [options, dispatch] = useChapterOptions(mangaId); const { data: chaptersData, mutate, isLoading } = requestManager.useGetMangaChapters(mangaId); const chapters = useMemo(() => chaptersData ?? [], [chaptersData]); + const mangaChapterIds = useMemo(() => chapters.map((chapter) => chapter.id), [chapters]); useEffect(() => { if (prevQueueRef.current && queue) { @@ -154,17 +156,20 @@ const ChapterList: React.FC = ({ mangaId }) => { if (action === 'download') { actionPromise = requestManager.addChaptersToDownloadQueue(chapterIds).response; } else { - const change: BatchChaptersChange = {}; + const change: UpdateChapterPatchInput = {}; - if (action === 'delete') change.delete = true; - else if (action === 'bookmark') change.isBookmarked = true; + if (action === 'bookmark') change.isBookmarked = true; else if (action === 'unbookmark') change.isBookmarked = false; else if (action === 'mark_as_read' || action === 'mark_as_unread') { change.isRead = action === 'mark_as_read'; change.lastPageRead = 0; } - actionPromise = requestManager.updateChapters(chapterIds, change).response; + if (action === 'delete') { + actionPromise = requestManager.deleteDownloadedChapters(chapterIds).response; + } else { + actionPromise = requestManager.updateChapters(chapterIds, change).response; + } } actionPromise @@ -266,6 +271,7 @@ const ChapterList: React.FC = ({ mangaId }) => { return ( mutate()} onSelect={() => handleSelection(index)} diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index d27d3330..85f81298 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -23,7 +23,6 @@ import { import { OperationVariables } from '@apollo/client/core'; import { BackupValidationResult, - BatchChaptersChange, ICategory, IChapter, IManga, @@ -42,6 +41,10 @@ import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts'; import { CheckForServerUpdatesQuery, CheckForServerUpdatesQueryVariables, + DeleteDownloadedChapterMutation, + DeleteDownloadedChapterMutationVariables, + DeleteDownloadedChaptersMutation, + DeleteDownloadedChaptersMutationVariables, GetAboutQuery, GetAboutQueryVariables, GetExtensionsFetchMutation, @@ -56,9 +59,16 @@ import { GetSourcesQueryVariables, InstallExternalExtensionMutation, InstallExternalExtensionMutationVariables, + SetChapterMetadataMutation, + SetChapterMetadataMutationVariables, SetGlobalMetadataMutation, SetGlobalMetadataMutationVariables, SetMangaMetadataMutation, + UpdateChapterMutation, + UpdateChapterMutationVariables, + UpdateChapterPatchInput, + UpdateChaptersMutation, + UpdateChaptersMutationVariables, UpdateExtensionMutation, UpdateExtensionMutationVariables, UpdateExtensionPatchInput, @@ -82,6 +92,9 @@ import { SET_MANGA_METADATA, UPDATE_MANGA, UPDATE_MANGA_CATEGORIES } from '@/lib import { GET_MANGA, GET_MANGAS } from '@/lib/graphql/queries/MangaQuery.ts'; import { GET_CATEGORIES, GET_CATEGORY, GET_CATEGORY_MANGAS } from '@/lib/graphql/queries/CategoryQuery.ts'; import { GET_SOURCE_MANGAS_FETCH } from '@/lib/graphql/mutations/SourceMutation.ts'; +import { DELETE_DOWNLOADED_CHAPTER, DELETE_DOWNLOADED_CHAPTERS } from '@/lib/graphql/mutations/DownloaderMutation.ts'; +import { GET_CHAPTER, GET_CHAPTERS } from '@/lib/graphql/queries/ChapterQuery.ts'; +import { SET_CHAPTER_METADATA, UPDATE_CHAPTER, UPDATE_CHAPTERS } from '@/lib/graphql/mutations/ChapterMutation.ts'; enum SWRHttpMethod { SWR_GET, @@ -747,27 +760,47 @@ export class RequestManager { return this.doRequest(HttpMethod.GET, `manga/${mangaId}/chapter/${chapterIndex}`); } - public deleteDownloadedChapter(mangaId: number | string, chapterIndex: number | string): AbortableAxiosResponse { - return this.doRequest(HttpMethod.DELETE, `manga/${mangaId}/chapter/${chapterIndex}`); + public deleteDownloadedChapter(id: number): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + DELETE_DOWNLOADED_CHAPTER, + { input: { id } }, + { refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORY_MANGAS, GET_CHAPTERS] }, + ); + } + + public deleteDownloadedChapters(ids: number[]): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + DELETE_DOWNLOADED_CHAPTERS, + { input: { ids } }, + { refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORY_MANGAS, GET_CHAPTERS] }, + ); } public updateChapter( - mangaId: number | string, - chapterIndex: number | string, - change: { read?: boolean; bookmarked?: boolean; markPrevRead?: boolean; lastPageRead?: number } = {}, - ): AbortableAxiosResponse { - return this.doRequest(HttpMethod.PATCH, `manga/${mangaId}/chapter/${chapterIndex}`, { formData: change }); + id: number, + patch: UpdateChapterPatchInput, + ): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + UPDATE_CHAPTER, + { input: { id, patch } }, + { refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORY_MANGAS, GET_CHAPTER, GET_CHAPTERS] }, + ); } public setChapterMeta( - mangaId: number | string, - chapterIndex: number | string, + chapterId: number, key: string, value: any, - ): AbortableAxiosResponse { - return this.doRequest(HttpMethod.PATCH, `manga/${mangaId}/chapter/${chapterIndex}/meta`, { - formData: { key, value }, - }); + ): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + SET_CHAPTER_METADATA, + { input: { meta: { chapterId, key, value: `${value}` } } }, + { refetchQueries: [GET_CHAPTER, GET_CHAPTERS] }, + ); } public getChapterPageUrl(mangaId: number | string, chapterIndex: number | string, page: number): string { @@ -777,8 +810,16 @@ export class RequestManager { ); } - public updateChapters(chapterIds: number[], change: BatchChaptersChange): AbortableAxiosResponse { - return this.doRequest(HttpMethod.POST, `chapter/batch`, { data: { chapterIds, change } }); + public updateChapters( + ids: number[], + patch: UpdateChapterPatchInput, + ): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + UPDATE_CHAPTERS, + { input: { ids, patch } }, + { refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORY_MANGAS, GET_CHAPTER, GET_CHAPTERS] }, + ); } public useGetCategories(swrOptions?: SWROptions): AbortableSWRResponse { diff --git a/src/screens/DownloadQueue.tsx b/src/screens/DownloadQueue.tsx index adbb469f..21b8dd8b 100644 --- a/src/screens/DownloadQueue.tsx +++ b/src/screens/DownloadQueue.tsx @@ -74,7 +74,7 @@ const DownloadQueue: React.FC = () => { requestManager.removeChapterFromDownloadQueue(chapter.mangaId, chapter.index).response, // delete partial download, should be handle server side? // bug: The folder and the last image downloaded are not deleted - requestManager.deleteDownloadedChapter(chapter.mangaId, chapter.index).response, + requestManager.deleteDownloadedChapter(chapter.id).response, ]); } catch (error) { makeToast(t('download.queue.error.label.failed_to_remove'), 'error'); diff --git a/src/screens/Reader.tsx b/src/screens/Reader.tsx index ac2489f5..d86b65a9 100644 --- a/src/screens/Reader.tsx +++ b/src/screens/Reader.tsx @@ -85,7 +85,7 @@ const initialChapter = { chapterCount: 0, lastPageRead: 0, name: 'Loading...', -}; +} as IChapter; export default function Reader() { const { t } = useTranslation(); @@ -207,19 +207,19 @@ export default function Reader() { // do not mutate the chapter, this will cause the page to jump around due to always scrolling to the last read page if (curPage !== -1) { - requestManager.updateChapter(manga.id, chapter.index, { lastPageRead: curPage }); + requestManager.updateChapter(chapter.id, { lastPageRead: curPage }); } if (curPage === chapter.pageCount - 1) { - requestManager.updateChapter(manga.id, chapter.index, { read: true }); + requestManager.updateChapter(chapter.id, { isRead: true }); } }, [curPage]); const nextChapter = useCallback(() => { if (chapter.index < chapter.chapterCount) { - requestManager.updateChapter(manga.id, chapter.index, { + requestManager.updateChapter(chapter.id, { lastPageRead: chapter.pageCount - 1, - read: true, + isRead: true, }); openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) => diff --git a/src/typings.ts b/src/typings.ts index f3054f94..a052f78c 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -368,13 +368,6 @@ export interface LibraryOptions { showTabSize: boolean; } -export interface BatchChaptersChange { - delete?: boolean; - isRead?: boolean; - isBookmarked?: boolean; - lastPageRead?: number; -} - export type UpdateCheck = { channel: 'Stable' | 'Preview'; tag: string; diff --git a/src/util/metadata.ts b/src/util/metadata.ts index acd2233d..dd69538b 100644 --- a/src/util/metadata.ts +++ b/src/util/metadata.ts @@ -315,7 +315,7 @@ export const requestUpdateMetadataValue = async ( // eslint-disable-next-line no-case-declarations const { manga, chapter } = metadataHolder as IMangaChapter; endpoint = `manga/${manga.id}/chapter/${chapter.index}/meta`; - await requestManager.setChapterMeta(manga.id, chapter.index, metadataKey, value).response; + await requestManager.setChapterMeta(chapter.id, metadataKey, value).response; break; case 'global': endpoint = 'meta';