Use gql for "updating chapters"

This commit is contained in:
schroda
2023-09-01 20:36:40 +02:00
parent c75e184c57
commit f0d55c01c8
7 changed files with 99 additions and 44 deletions

View File

@@ -31,9 +31,11 @@ import { IChapter, IDownloadChapter } from '@/typings';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import { getUploadDateString } from '@/util/date'; import { getUploadDateString } from '@/util/date';
import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator'; import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator';
import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
interface IProps { interface IProps {
chapter: IChapter; chapter: IChapter;
chapterIds: number[];
triggerChaptersUpdate: () => void; triggerChaptersUpdate: () => void;
downloadChapter: IDownloadChapter | undefined; downloadChapter: IDownloadChapter | undefined;
showChapterNumber: boolean; showChapterNumber: boolean;
@@ -45,7 +47,15 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); 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 isSelecting = selected !== null;
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
@@ -62,13 +72,20 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
setAnchorEl(null); setAnchorEl(null);
}; };
const sendChange = (key: string, value: any) => { type UpdatePatchInput = UpdateChapterPatchInput & { markPrevRead?: boolean };
const sendChange = <Key extends keyof UpdatePatchInput>(key: Key, value: UpdatePatchInput[Key]) => {
handleClose(); handleClose();
if (key === 'markPrevRead') {
const index = chapterIds.findIndex((chapterId) => chapterId === chapter.id);
requestManager.updateChapters(chapterIds.slice(index, -1), { isRead: true });
return;
}
requestManager requestManager
.updateChapter(chapter.mangaId, chapter.index, { .updateChapter(chapter.id, {
[key]: value, [key]: value,
lastPageRead: key === 'read' ? 0 : undefined, lastPageRead: key === 'isRead' ? 0 : undefined,
}) })
.response.then(() => triggerChaptersUpdate()); .response.then(() => triggerChaptersUpdate());
}; };
@@ -79,9 +96,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
}; };
const deleteChapter = () => { const deleteChapter = () => {
requestManager requestManager.deleteDownloadedChapter(chapter.id).response.then(() => triggerChaptersUpdate());
.deleteDownloadedChapter(chapter.mangaId, chapter.index)
.response.then(() => triggerChaptersUpdate());
handleClose(); handleClose();
}; };
@@ -177,7 +192,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
<ListItemText>{t('chapter.action.download.add.label.action')}</ListItemText> <ListItemText>{t('chapter.action.download.add.label.action')}</ListItemText>
</MenuItem> </MenuItem>
)} )}
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}> <MenuItem onClick={() => sendChange('isBookmarked', !chapter.bookmarked)}>
<ListItemIcon> <ListItemIcon>
{chapter.bookmarked && <BookmarkRemove fontSize="small" />} {chapter.bookmarked && <BookmarkRemove fontSize="small" />}
{!chapter.bookmarked && <BookmarkAdd fontSize="small" />} {!chapter.bookmarked && <BookmarkAdd fontSize="small" />}
@@ -187,7 +202,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
{!chapter.bookmarked && t('chapter.action.bookmark.add.label.action')} {!chapter.bookmarked && t('chapter.action.bookmark.add.label.action')}
</ListItemText> </ListItemText>
</MenuItem> </MenuItem>
<MenuItem onClick={() => sendChange('read', !chapter.read)}> <MenuItem onClick={() => sendChange('isRead', !chapter.read)}>
<ListItemIcon> <ListItemIcon>
{chapter.read && <RemoveDone fontSize="small" />} {chapter.read && <RemoveDone fontSize="small" />}
{!chapter.read && <Done fontSize="small" />} {!chapter.read && <Done fontSize="small" />}

View File

@@ -11,7 +11,7 @@ import Typography from '@mui/material/Typography';
import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react'; import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
import { useTranslation } from 'react-i18next'; 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 requestManager from '@/lib/requests/RequestManager.ts';
import useSubscription from '@/components/library/useSubscription'; import useSubscription from '@/components/library/useSubscription';
import ChapterCard from '@/components/manga/ChapterCard'; import ChapterCard from '@/components/manga/ChapterCard';
@@ -22,6 +22,7 @@ import makeToast from '@/components/util/Toast';
import ChaptersToolbarMenu from '@/components/manga/ChaptersToolbarMenu'; import ChaptersToolbarMenu from '@/components/manga/ChaptersToolbarMenu';
import SelectionFAB from '@/components/manga/SelectionFAB'; import SelectionFAB from '@/components/manga/SelectionFAB';
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab';
import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({ const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
listStyle: 'none', listStyle: 'none',
@@ -87,6 +88,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
const [options, dispatch] = useChapterOptions(mangaId); const [options, dispatch] = useChapterOptions(mangaId);
const { data: chaptersData, mutate, isLoading } = requestManager.useGetMangaChapters(mangaId); const { data: chaptersData, mutate, isLoading } = requestManager.useGetMangaChapters(mangaId);
const chapters = useMemo(() => chaptersData ?? [], [chaptersData]); const chapters = useMemo(() => chaptersData ?? [], [chaptersData]);
const mangaChapterIds = useMemo(() => chapters.map((chapter) => chapter.id), [chapters]);
useEffect(() => { useEffect(() => {
if (prevQueueRef.current && queue) { if (prevQueueRef.current && queue) {
@@ -154,17 +156,20 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
if (action === 'download') { if (action === 'download') {
actionPromise = requestManager.addChaptersToDownloadQueue(chapterIds).response; actionPromise = requestManager.addChaptersToDownloadQueue(chapterIds).response;
} else { } else {
const change: BatchChaptersChange = {}; const change: UpdateChapterPatchInput = {};
if (action === 'delete') change.delete = true; if (action === 'bookmark') change.isBookmarked = true;
else if (action === 'bookmark') change.isBookmarked = true;
else if (action === 'unbookmark') change.isBookmarked = false; else if (action === 'unbookmark') change.isBookmarked = false;
else if (action === 'mark_as_read' || action === 'mark_as_unread') { else if (action === 'mark_as_read' || action === 'mark_as_unread') {
change.isRead = action === 'mark_as_read'; change.isRead = action === 'mark_as_read';
change.lastPageRead = 0; 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 actionPromise
@@ -266,6 +271,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
return ( return (
<ChapterCard <ChapterCard
{...chaptersWithMeta[index]} {...chaptersWithMeta[index]}
chapterIds={mangaChapterIds}
showChapterNumber={options.showChapterNumber} showChapterNumber={options.showChapterNumber}
triggerChaptersUpdate={() => mutate()} triggerChaptersUpdate={() => mutate()}
onSelect={() => handleSelection(index)} onSelect={() => handleSelection(index)}

View File

@@ -23,7 +23,6 @@ import {
import { OperationVariables } from '@apollo/client/core'; import { OperationVariables } from '@apollo/client/core';
import { import {
BackupValidationResult, BackupValidationResult,
BatchChaptersChange,
ICategory, ICategory,
IChapter, IChapter,
IManga, IManga,
@@ -42,6 +41,10 @@ import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts';
import { import {
CheckForServerUpdatesQuery, CheckForServerUpdatesQuery,
CheckForServerUpdatesQueryVariables, CheckForServerUpdatesQueryVariables,
DeleteDownloadedChapterMutation,
DeleteDownloadedChapterMutationVariables,
DeleteDownloadedChaptersMutation,
DeleteDownloadedChaptersMutationVariables,
GetAboutQuery, GetAboutQuery,
GetAboutQueryVariables, GetAboutQueryVariables,
GetExtensionsFetchMutation, GetExtensionsFetchMutation,
@@ -56,9 +59,16 @@ import {
GetSourcesQueryVariables, GetSourcesQueryVariables,
InstallExternalExtensionMutation, InstallExternalExtensionMutation,
InstallExternalExtensionMutationVariables, InstallExternalExtensionMutationVariables,
SetChapterMetadataMutation,
SetChapterMetadataMutationVariables,
SetGlobalMetadataMutation, SetGlobalMetadataMutation,
SetGlobalMetadataMutationVariables, SetGlobalMetadataMutationVariables,
SetMangaMetadataMutation, SetMangaMetadataMutation,
UpdateChapterMutation,
UpdateChapterMutationVariables,
UpdateChapterPatchInput,
UpdateChaptersMutation,
UpdateChaptersMutationVariables,
UpdateExtensionMutation, UpdateExtensionMutation,
UpdateExtensionMutationVariables, UpdateExtensionMutationVariables,
UpdateExtensionPatchInput, 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_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_CATEGORIES, GET_CATEGORY, GET_CATEGORY_MANGAS } from '@/lib/graphql/queries/CategoryQuery.ts';
import { GET_SOURCE_MANGAS_FETCH } from '@/lib/graphql/mutations/SourceMutation.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 { enum SWRHttpMethod {
SWR_GET, SWR_GET,
@@ -747,27 +760,47 @@ export class RequestManager {
return this.doRequest(HttpMethod.GET, `manga/${mangaId}/chapter/${chapterIndex}`); return this.doRequest(HttpMethod.GET, `manga/${mangaId}/chapter/${chapterIndex}`);
} }
public deleteDownloadedChapter(mangaId: number | string, chapterIndex: number | string): AbortableAxiosResponse { public deleteDownloadedChapter(id: number): AbortableApolloMutationResponse<DeleteDownloadedChapterMutation> {
return this.doRequest(HttpMethod.DELETE, `manga/${mangaId}/chapter/${chapterIndex}`); return this.doRequestNew<DeleteDownloadedChapterMutation, DeleteDownloadedChapterMutationVariables>(
GQLMethod.MUTATION,
DELETE_DOWNLOADED_CHAPTER,
{ input: { id } },
{ refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORY_MANGAS, GET_CHAPTERS] },
);
}
public deleteDownloadedChapters(ids: number[]): AbortableApolloMutationResponse<DeleteDownloadedChaptersMutation> {
return this.doRequestNew<DeleteDownloadedChaptersMutation, DeleteDownloadedChaptersMutationVariables>(
GQLMethod.MUTATION,
DELETE_DOWNLOADED_CHAPTERS,
{ input: { ids } },
{ refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORY_MANGAS, GET_CHAPTERS] },
);
} }
public updateChapter( public updateChapter(
mangaId: number | string, id: number,
chapterIndex: number | string, patch: UpdateChapterPatchInput,
change: { read?: boolean; bookmarked?: boolean; markPrevRead?: boolean; lastPageRead?: number } = {}, ): AbortableApolloMutationResponse<UpdateChapterMutation> {
): AbortableAxiosResponse { return this.doRequestNew<UpdateChapterMutation, UpdateChapterMutationVariables>(
return this.doRequest(HttpMethod.PATCH, `manga/${mangaId}/chapter/${chapterIndex}`, { formData: change }); GQLMethod.MUTATION,
UPDATE_CHAPTER,
{ input: { id, patch } },
{ refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORY_MANGAS, GET_CHAPTER, GET_CHAPTERS] },
);
} }
public setChapterMeta( public setChapterMeta(
mangaId: number | string, chapterId: number,
chapterIndex: number | string,
key: string, key: string,
value: any, value: any,
): AbortableAxiosResponse { ): AbortableApolloMutationResponse<SetChapterMetadataMutation> {
return this.doRequest(HttpMethod.PATCH, `manga/${mangaId}/chapter/${chapterIndex}/meta`, { return this.doRequestNew<SetChapterMetadataMutation, SetChapterMetadataMutationVariables>(
formData: { key, value }, 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 { 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 { public updateChapters(
return this.doRequest(HttpMethod.POST, `chapter/batch`, { data: { chapterIds, change } }); ids: number[],
patch: UpdateChapterPatchInput,
): AbortableApolloMutationResponse<UpdateChaptersMutation> {
return this.doRequestNew<UpdateChaptersMutation, UpdateChaptersMutationVariables>(
GQLMethod.MUTATION,
UPDATE_CHAPTERS,
{ input: { ids, patch } },
{ refetchQueries: [GET_MANGA, GET_MANGAS, GET_CATEGORY_MANGAS, GET_CHAPTER, GET_CHAPTERS] },
);
} }
public useGetCategories(swrOptions?: SWROptions<ICategory[]>): AbortableSWRResponse<ICategory[]> { public useGetCategories(swrOptions?: SWROptions<ICategory[]>): AbortableSWRResponse<ICategory[]> {

View File

@@ -74,7 +74,7 @@ const DownloadQueue: React.FC = () => {
requestManager.removeChapterFromDownloadQueue(chapter.mangaId, chapter.index).response, requestManager.removeChapterFromDownloadQueue(chapter.mangaId, chapter.index).response,
// delete partial download, should be handle server side? // delete partial download, should be handle server side?
// bug: The folder and the last image downloaded are not deleted // 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) { } catch (error) {
makeToast(t('download.queue.error.label.failed_to_remove'), 'error'); makeToast(t('download.queue.error.label.failed_to_remove'), 'error');

View File

@@ -85,7 +85,7 @@ const initialChapter = {
chapterCount: 0, chapterCount: 0,
lastPageRead: 0, lastPageRead: 0,
name: 'Loading...', name: 'Loading...',
}; } as IChapter;
export default function Reader() { export default function Reader() {
const { t } = useTranslation(); 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 // 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) { if (curPage !== -1) {
requestManager.updateChapter(manga.id, chapter.index, { lastPageRead: curPage }); requestManager.updateChapter(chapter.id, { lastPageRead: curPage });
} }
if (curPage === chapter.pageCount - 1) { if (curPage === chapter.pageCount - 1) {
requestManager.updateChapter(manga.id, chapter.index, { read: true }); requestManager.updateChapter(chapter.id, { isRead: true });
} }
}, [curPage]); }, [curPage]);
const nextChapter = useCallback(() => { const nextChapter = useCallback(() => {
if (chapter.index < chapter.chapterCount) { if (chapter.index < chapter.chapterCount) {
requestManager.updateChapter(manga.id, chapter.index, { requestManager.updateChapter(chapter.id, {
lastPageRead: chapter.pageCount - 1, lastPageRead: chapter.pageCount - 1,
read: true, isRead: true,
}); });
openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) => openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) =>

View File

@@ -368,13 +368,6 @@ export interface LibraryOptions {
showTabSize: boolean; showTabSize: boolean;
} }
export interface BatchChaptersChange {
delete?: boolean;
isRead?: boolean;
isBookmarked?: boolean;
lastPageRead?: number;
}
export type UpdateCheck = { export type UpdateCheck = {
channel: 'Stable' | 'Preview'; channel: 'Stable' | 'Preview';
tag: string; tag: string;

View File

@@ -315,7 +315,7 @@ export const requestUpdateMetadataValue = async (
// eslint-disable-next-line no-case-declarations // eslint-disable-next-line no-case-declarations
const { manga, chapter } = metadataHolder as IMangaChapter; const { manga, chapter } = metadataHolder as IMangaChapter;
endpoint = `manga/${manga.id}/chapter/${chapter.index}/meta`; 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; break;
case 'global': case 'global':
endpoint = 'meta'; endpoint = 'meta';