[Codegen] Use gql for "loading chapters"

This commit is contained in:
schroda
2023-09-30 16:55:35 +02:00
parent 78049282a7
commit aad87463f3
16 changed files with 500 additions and 230 deletions

View File

@@ -27,16 +27,15 @@ import Typography from '@mui/material/Typography';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IChapter, IDownloadChapter } from '@/typings'; import { 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'; import { ChapterType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
interface IProps { interface IProps {
chapter: IChapter; chapter: ChapterType;
chapterIds: number[]; chapterIds: number[];
triggerChaptersUpdate: () => void;
downloadChapter: IDownloadChapter | undefined; downloadChapter: IDownloadChapter | undefined;
showChapterNumber: boolean; showChapterNumber: boolean;
onSelect: (selected: boolean) => void; onSelect: (selected: boolean) => void;
@@ -47,15 +46,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const { const { chapter, chapterIds, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
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);
@@ -82,12 +73,10 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
return; return;
} }
requestManager requestManager.updateChapter(chapter.id, {
.updateChapter(chapter.id, { [key]: value,
[key]: value, lastPageRead: key === 'isRead' ? 0 : undefined,
lastPageRead: key === 'isRead' ? 0 : undefined, });
})
.response.then(() => triggerChaptersUpdate());
}; };
const downloadChapter = () => { const downloadChapter = () => {
@@ -96,7 +85,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
}; };
const deleteChapter = () => { const deleteChapter = () => {
requestManager.deleteDownloadedChapter(chapter.id).response.then(() => triggerChaptersUpdate()); requestManager.deleteDownloadedChapter(chapter.id);
handleClose(); handleClose();
}; };
@@ -113,8 +102,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
} }
}; };
const isDownloaded = chapter.downloaded; const { isDownloaded } = chapter;
const canBeDownloaded = !chapter.downloaded && dc === undefined; const canBeDownloaded = !chapter.isDownloaded && dc === undefined;
return ( return (
<li> <li>
@@ -126,9 +115,9 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
> >
<CardActionArea <CardActionArea
component={Link} component={Link}
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`} to={`/manga/${chapter.manga.id}/chapter/${chapter.sourceOrder}`}
style={{ style={{
color: theme.palette.text[chapter.read ? 'disabled' : 'primary'], color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
}} }}
onClick={handleClick} onClick={handleClick}
> >
@@ -143,7 +132,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
> >
<Stack direction="column" flex={1}> <Stack direction="column" flex={1}>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
{chapter.bookmarked && ( {chapter.isBookmarked && (
<BookmarkIcon <BookmarkIcon
color="primary" color="primary"
sx={{ mr: 0.5, position: 'relative', top: '0.15em' }} sx={{ mr: 0.5, position: 'relative', top: '0.15em' }}
@@ -153,7 +142,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
</Typography> </Typography>
<Typography variant="caption">{chapter.scanlator}</Typography> <Typography variant="caption">{chapter.scanlator}</Typography>
<Typography variant="caption"> <Typography variant="caption">
{getUploadDateString(chapter.uploadDate)} {getUploadDateString(Number(chapter.uploadDate ?? 0))}
{isDownloaded && `${t('chapter.status.label.downloaded')}`} {isDownloaded && `${t('chapter.status.label.downloaded')}`}
</Typography> </Typography>
</Stack> </Stack>
@@ -192,24 +181,24 @@ 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('isBookmarked', !chapter.bookmarked)}> <MenuItem onClick={() => sendChange('isBookmarked', !chapter.isBookmarked)}>
<ListItemIcon> <ListItemIcon>
{chapter.bookmarked && <BookmarkRemove fontSize="small" />} {chapter.isBookmarked && <BookmarkRemove fontSize="small" />}
{!chapter.bookmarked && <BookmarkAdd fontSize="small" />} {!chapter.isBookmarked && <BookmarkAdd fontSize="small" />}
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>
{chapter.bookmarked && t('chapter.action.bookmark.remove.label.action')} {chapter.isBookmarked && t('chapter.action.bookmark.remove.label.action')}
{!chapter.bookmarked && t('chapter.action.bookmark.add.label.action')} {!chapter.isBookmarked && t('chapter.action.bookmark.add.label.action')}
</ListItemText> </ListItemText>
</MenuItem> </MenuItem>
<MenuItem onClick={() => sendChange('isRead', !chapter.read)}> <MenuItem onClick={() => sendChange('isRead', !chapter.isRead)}>
<ListItemIcon> <ListItemIcon>
{chapter.read && <RemoveDone fontSize="small" />} {chapter.isRead && <RemoveDone fontSize="small" />}
{!chapter.read && <Done fontSize="small" />} {!chapter.isRead && <Done fontSize="small" />}
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>
{chapter.read && t('chapter.action.mark_as_read.remove.label.action')} {chapter.isRead && t('chapter.action.mark_as_read.remove.label.action')}
{!chapter.read && t('chapter.action.mark_as_read.add.label.action.current')} {!chapter.isRead && t('chapter.action.mark_as_read.add.label.action.current')}
</ListItemText> </ListItemText>
</MenuItem> </MenuItem>
<MenuItem onClick={() => sendChange('markPrevRead', true)}> <MenuItem onClick={() => sendChange('markPrevRead', true)}>

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 { IChapter, IDownloadChapter, IQueue, TranslationKey } from '@/typings'; import { 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,7 +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'; import { ChapterType, MangaType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({ const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
listStyle: 'none', listStyle: 'none',
@@ -69,25 +69,29 @@ const actionsStrings: {
}; };
export interface IChapterWithMeta { export interface IChapterWithMeta {
chapter: IChapter; chapter: ChapterType;
downloadChapter: IDownloadChapter | undefined; downloadChapter: IDownloadChapter | undefined;
selected: boolean | null; selected: boolean | null;
} }
interface IProps { interface IProps {
mangaId: string; manga: MangaType;
isRefreshing: boolean;
} }
const ChapterList: React.FC<IProps> = ({ mangaId }) => { const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [selection, setSelection] = useState<number[] | null>(null); const [selection, setSelection] = useState<number[] | null>(null);
const prevQueueRef = useRef<IDownloadChapter[]>(); const prevQueueRef = useRef<IDownloadChapter[]>();
const queue = useSubscription<IQueue>('downloads').data?.queue; const queue = useSubscription<IQueue>('downloads').data?.queue;
const [options, dispatch] = useChapterOptions(mangaId); const [options, dispatch] = useChapterOptions(manga.id);
const { data: chaptersData, mutate, isLoading } = requestManager.useGetMangaChapters(mangaId); const { data: chaptersData, loading: isLoading, refetch } = requestManager.useGetMangaChapters(manga.id);
const chapters = useMemo(() => chaptersData ?? [], [chaptersData]); const chapters = useMemo(
() => (chaptersData?.chapters.nodes as ChapterType[]) ?? [],
[chaptersData?.chapters.nodes],
);
const mangaChapterIds = useMemo(() => chapters.map((chapter) => chapter.id), [chapters]); const mangaChapterIds = useMemo(() => chapters.map((chapter) => chapter.id), [chapters]);
useEffect(() => { useEffect(() => {
@@ -102,26 +106,17 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
}); });
if (changedDownloads.length > 0) { if (changedDownloads.length > 0) {
mutate(); refetch();
} }
} }
prevQueueRef.current = queue; prevQueueRef.current = queue;
}, [queue]); }, [queue]);
const visibleChapters = useMemo( const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
() => filterAndSortChapters(chapters, options), //
[chapters, options],
);
const firstUnreadChapter = useMemo( const nextChapterIndexToRead = (manga.lastReadChapter?.sourceOrder ?? 0) + 1;
() => const isLatestChapterRead = manga.chapters.totalCount === manga.lastReadChapter?.sourceOrder;
chapters
.slice()
.reverse()
.find((chapter) => !chapter.read),
[chapters],
);
const handleSelection = (index: number) => { const handleSelection = (index: number) => {
const chapter = visibleChapters[index]; const chapter = visibleChapters[index];
@@ -174,11 +169,49 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
actionPromise actionPromise
.then(() => makeToast(t(actionsStrings[action].success, { count: chapterIds.length }), 'success')) .then(() => makeToast(t(actionsStrings[action].success, { count: chapterIds.length }), 'success'))
.then(() => mutate())
.catch(() => makeToast(t(actionsStrings[action].error, { count: chapterIds.length }), 'error')); .catch(() => makeToast(t(actionsStrings[action].error, { count: chapterIds.length }), 'error'));
}; };
if (isLoading) { const noChaptersFound = chapters.length === 0;
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
const chaptersWithMeta: IChapterWithMeta[] = useMemo(
() =>
visibleChapters.map((chapter) => {
const downloadChapter = queue?.find(
(cd) => cd.chapterIndex === chapter.sourceOrder && cd.mangaId === chapter.manga.id,
);
const selected = selection?.includes(chapter.id) ?? null;
return {
chapter,
downloadChapter,
selected,
};
}),
[queue, selection, visibleChapters],
);
const selectedChapters = useMemo(() => {
if (!selection) {
return null;
}
return chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
}, [selection, chapters]);
const chapterListFAB = useMemo(() => {
if (selectedChapters) {
return <SelectionFAB selectedChapters={selectedChapters} onAction={handleFabAction} />;
}
if (!isLatestChapterRead) {
return <ResumeFab chapterIndex={nextChapterIndexToRead} mangaId={manga.id} />;
}
return null;
}, [selectedChapters, isLatestChapterRead]);
if (isLoading || (noChaptersFound && isRefreshing)) {
return ( return (
<div <div
style={{ style={{
@@ -192,24 +225,6 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
); );
} }
const noChaptersFound = chapters.length === 0;
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
const chaptersWithMeta: IChapterWithMeta[] = visibleChapters.map((chapter) => {
const downloadChapter = queue?.find(
(cd) => cd.chapterIndex === chapter.index && cd.mangaId === chapter.mangaId,
);
const selected = selection?.includes(chapter.id) ?? null;
return {
chapter,
downloadChapter,
selected,
};
});
const selectedChapters =
selection === null ? null : chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
return ( return (
<> <>
<Stack direction="column" sx={{ position: 'relative' }}> <Stack direction="column" sx={{ position: 'relative' }}>
@@ -273,7 +288,6 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
{...chaptersWithMeta[index]} {...chaptersWithMeta[index]}
chapterIds={mangaChapterIds} chapterIds={mangaChapterIds}
showChapterNumber={options.showChapterNumber} showChapterNumber={options.showChapterNumber}
triggerChaptersUpdate={() => mutate()}
onSelect={() => handleSelection(index)} onSelect={() => handleSelection(index)}
/> />
); );
@@ -282,11 +296,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
overscan={window.innerHeight * 0.5} overscan={window.innerHeight * 0.5}
/> />
</Stack> </Stack>
{selectedChapters !== null ? ( {chapterListFAB}
<SelectionFAB selectedChapters={selectedChapters} onAction={handleFabAction} />
) : (
firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={mangaId} />
)}
</> </>
); );
}; };

View File

@@ -9,25 +9,21 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { PlayArrow } from '@mui/icons-material'; import { PlayArrow } from '@mui/icons-material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IChapter } from '@/typings';
import StyledFab from '@/components/util/StyledFab'; import StyledFab from '@/components/util/StyledFab';
interface ResumeFABProps { interface ResumeFABProps {
chapter: IChapter; chapterIndex: number;
mangaId: string; mangaId: number;
} }
export default function ResumeFab(props: ResumeFABProps) { export default function ResumeFab(props: ResumeFABProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { const { chapterIndex, mangaId } = props;
chapter: { index },
mangaId,
} = props;
return ( return (
<StyledFab component={Link} variant="extended" color="primary" to={`/manga/${mangaId}/chapter/${index}`}> <StyledFab component={Link} variant="extended" color="primary" to={`/manga/${mangaId}/chapter/${chapterIndex}`}>
<PlayArrow /> <PlayArrow />
{index === 1 ? t('global.button.start') : t('global.button.resume')} {chapterIndex === 1 ? t('global.button.start') : t('global.button.resume')}
</StyledFab> </StyledFab>
); );
} }

View File

@@ -63,38 +63,38 @@ const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
<SelectionFABActionItem <SelectionFABActionItem
action="download" action="download"
matchingChapters={selectedChapters.filter( matchingChapters={selectedChapters.filter(
({ chapter: c, downloadChapter: dc }) => !c.downloaded && dc === undefined, ({ chapter: c, downloadChapter: dc }) => !c.isDownloaded && dc === undefined,
)} )}
onClick={handleAction} onClick={handleAction}
title={t('chapter.action.download.add.button.selected')} title={t('chapter.action.download.add.button.selected')}
/> />
<SelectionFABActionItem <SelectionFABActionItem
action="delete" action="delete"
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.downloaded)} matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isDownloaded)}
onClick={handleAction} onClick={handleAction}
title={t('chapter.action.download.delete.button.selected')} title={t('chapter.action.download.delete.button.selected')}
/> />
<SelectionFABActionItem <SelectionFABActionItem
action="bookmark" action="bookmark"
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.bookmarked)} matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.isBookmarked)}
onClick={handleAction} onClick={handleAction}
title={t('chapter.action.bookmark.add.button.selected')} title={t('chapter.action.bookmark.add.button.selected')}
/> />
<SelectionFABActionItem <SelectionFABActionItem
action="unbookmark" action="unbookmark"
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.bookmarked)} matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isBookmarked)}
onClick={handleAction} onClick={handleAction}
title={t('chapter.action.bookmark.remove.button.selected')} title={t('chapter.action.bookmark.remove.button.selected')}
/> />
<SelectionFABActionItem <SelectionFABActionItem
action="mark_as_read" action="mark_as_read"
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.read)} matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.isRead)}
onClick={handleAction} onClick={handleAction}
title={t('chapter.action.mark_as_read.add.button.selected')} title={t('chapter.action.mark_as_read.add.button.selected')}
/> />
<SelectionFABActionItem <SelectionFABActionItem
action="mark_as_unread" action="mark_as_unread"
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.read)} matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isRead)}
onClick={handleAction} onClick={handleAction}
title={t('chapter.action.mark_as_read.remove.button.selected')} title={t('chapter.action.mark_as_read.remove.button.selected')}
/> />

View File

@@ -7,8 +7,7 @@
*/ */
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { mutate } from 'swr'; import requestManager from '@/lib/requests/RequestManager.ts';
import requestManager, { RequestManager } from '@/lib/requests/RequestManager.ts';
export const useRefreshManga = (mangaId: string) => { export const useRefreshManga = (mangaId: string) => {
const [fetchingOnline, setFetchingOnline] = useState(false); const [fetchingOnline, setFetchingOnline] = useState(false);
@@ -16,14 +15,8 @@ export const useRefreshManga = (mangaId: string) => {
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
setFetchingOnline(true); setFetchingOnline(true);
await Promise.all([ await Promise.all([
requestManager.getMangaFetch(mangaId).response.then((res) => { requestManager.getMangaFetch(mangaId, { awaitRefetchQueries: true }).response,
mutate(`${RequestManager.API_VERSION}manga/${mangaId}`, res, { revalidate: false }); requestManager.getMangaChaptersFetch(mangaId, { awaitRefetchQueries: true }).response,
}),
requestManager.getMangaChapters(mangaId, true).response.then((res) =>
mutate(`${RequestManager.API_VERSION}manga/${mangaId}/chapters`, res, {
revalidate: false,
}),
),
]).finally(() => setFetchingOnline(false)); ]).finally(() => setFetchingOnline(false));
}, [mangaId]); }, [mangaId]);

View File

@@ -11,11 +11,11 @@ import {
ChapterListOptions, ChapterListOptions,
ChapterOptionsReducerAction, ChapterOptionsReducerAction,
ChapterSortMode, ChapterSortMode,
IChapter,
NullAndUndefined, NullAndUndefined,
TranslationKey, TranslationKey,
} from '@/typings'; } from '@/typings';
import { useReducerLocalStorage } from '@/util/useLocalStorage'; import { useReducerLocalStorage } from '@/util/useLocalStorage';
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
const defaultChapterOptions: ChapterListOptions = { const defaultChapterOptions: ChapterListOptions = {
active: false, active: false,
@@ -46,7 +46,7 @@ function chapterOptionsReducer(state: ChapterListOptions, actions: ChapterOption
} }
} }
export function unreadFilter(unread: NullAndUndefined<boolean>, { read: isChapterRead }: IChapter) { export function unreadFilter(unread: NullAndUndefined<boolean>, { isRead: isChapterRead }: ChapterType) {
switch (unread) { switch (unread) {
case true: case true:
return !isChapterRead; return !isChapterRead;
@@ -57,7 +57,7 @@ export function unreadFilter(unread: NullAndUndefined<boolean>, { read: isChapte
} }
} }
function downloadFilter(downloaded: NullAndUndefined<boolean>, { downloaded: chapterDownload }: IChapter) { function downloadFilter(downloaded: NullAndUndefined<boolean>, { isDownloaded: chapterDownload }: ChapterType) {
switch (downloaded) { switch (downloaded) {
case true: case true:
return chapterDownload; return chapterDownload;
@@ -68,7 +68,7 @@ function downloadFilter(downloaded: NullAndUndefined<boolean>, { downloaded: cha
} }
} }
function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>, { bookmarked: chapterBookmarked }: IChapter) { function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>, { isBookmarked: chapterBookmarked }: ChapterType) {
switch (bookmarked) { switch (bookmarked) {
case true: case true:
return chapterBookmarked; return chapterBookmarked;
@@ -79,7 +79,7 @@ function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>, { bookmarked: c
} }
} }
export function filterAndSortChapters(chapters: IChapter[], options: ChapterListOptions): IChapter[] { export function filterAndSortChapters(chapters: ChapterType[], options: ChapterListOptions): ChapterType[] {
const filtered = options.active const filtered = options.active
? chapters.filter( ? chapters.filter(
(chp) => (chp) =>
@@ -88,14 +88,17 @@ export function filterAndSortChapters(chapters: IChapter[], options: ChapterList
bookmarkedFilter(options.bookmarked, chp), bookmarkedFilter(options.bookmarked, chp),
) )
: [...chapters]; : [...chapters];
const Sorted = options.sortBy === 'fetchedAt' ? filtered.sort((a, b) => a.fetchedAt - b.fetchedAt) : filtered; const Sorted =
options.sortBy === 'fetchedAt'
? filtered.sort((a, b) => Number(a.fetchedAt ?? 0) - Number(b.fetchedAt ?? 0))
: filtered;
if (options.reverse) { if (options.reverse) {
Sorted.reverse(); Sorted.reverse();
} }
return Sorted; return Sorted;
} }
export const useChapterOptions = (mangaId: string) => export const useChapterOptions = (mangaId: number) =>
useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>( useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
chapterOptionsReducer, chapterOptionsReducer,
`${mangaId}filterOptions`, `${mangaId}filterOptions`,

View File

@@ -24,9 +24,9 @@ import ListItemText from '@mui/material/ListItemText';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import Collapse from '@mui/material/Collapse'; import Collapse from '@mui/material/Collapse';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ChapterOffset, IChapter, IReaderSettings } from '@/typings'; import { ChapterOffset, IReaderSettings } from '@/typings';
import ReaderSettingsOptions from '@/components/reader/ReaderSettingsOptions'; import ReaderSettingsOptions from '@/components/reader/ReaderSettingsOptions';
import { MangaType } from '@/lib/graphql/generated/graphql.ts'; import { ChapterType, MangaType } from '@/lib/graphql/generated/graphql.ts';
const Root = styled('div')(({ theme }) => ({ const Root = styled('div')(({ theme }) => ({
top: 0, top: 0,
@@ -115,8 +115,8 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
interface IProps { interface IProps {
settings: IReaderSettings; settings: IReaderSettings;
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void; setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
manga: Pick<MangaType, 'id'>; manga: MangaType;
chapter: IChapter; chapter: ChapterType;
curPage: number; curPage: number;
scrollToPage: (page: number) => void; scrollToPage: (page: number) => void;
openNextChapter: (offset: ChapterOffset, setHistory: (nextChapterIndex: number) => void) => Promise<void>; openNextChapter: (offset: ChapterOffset, setHistory: (nextChapterIndex: number) => void) => Promise<void>;
@@ -304,7 +304,7 @@ export default function ReaderNavBar(props: IProps) {
<IconButton <IconButton
title={t('reader.button.previous_chapter')} title={t('reader.button.previous_chapter')}
sx={{ gridArea: 'pre' }} sx={{ gridArea: 'pre' }}
disabled={disableChapterNavButtons || chapter.index <= 1} disabled={disableChapterNavButtons || chapter.sourceOrder <= 1}
onClick={() => onClick={() =>
openNextChapter(ChapterOffset.PREV, (prevChapterIndex) => { openNextChapter(ChapterOffset.PREV, (prevChapterIndex) => {
navigate(`/manga/${manga.id}/chapter/${prevChapterIndex}`, { navigate(`/manga/${manga.id}/chapter/${prevChapterIndex}`, {
@@ -322,11 +322,11 @@ export default function ReaderNavBar(props: IProps) {
<FormControl <FormControl
sx={{ gridArea: 'current' }} sx={{ gridArea: 'current' }}
size="small" size="small"
disabled={disableChapterNavButtons || chapter.index < 1} disabled={disableChapterNavButtons || chapter.sourceOrder < 1}
> >
<Select <Select
MenuProps={MenuProps} MenuProps={MenuProps}
value={chapter.index >= 1 ? chapter.index : ''} value={chapter.sourceOrder >= 1 ? chapter.sourceOrder : ''}
displayEmpty displayEmpty
onChange={({ target: { value: selectedChapter } }) => { onChange={({ target: { value: selectedChapter } }) => {
navigate(`/manga/${manga.id}/chapter/${selectedChapter}`, { navigate(`/manga/${manga.id}/chapter/${selectedChapter}`, {
@@ -338,7 +338,7 @@ export default function ReaderNavBar(props: IProps) {
}); });
}} }}
> >
{Array(Math.max(0, chapter.chapterCount)) {Array(Math.max(0, manga.chapters.totalCount))
.fill(1) .fill(1)
.map((ignoreValue, index) => ( .map((ignoreValue, index) => (
<MenuItem key={`Chapter#${index + 1}`} value={index + 1}>{`${t( <MenuItem key={`Chapter#${index + 1}`} value={index + 1}>{`${t(
@@ -352,8 +352,8 @@ export default function ReaderNavBar(props: IProps) {
sx={{ gridArea: 'next' }} sx={{ gridArea: 'next' }}
disabled={ disabled={
disableChapterNavButtons || disableChapterNavButtons ||
chapter.index < 1 || chapter.sourceOrder < 1 ||
chapter.index >= chapter.chapterCount chapter.sourceOrder >= manga.chapters.totalCount
} }
onClick={() => { onClick={() => {
openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) => openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) =>

View File

@@ -1,4 +1,5 @@
import { FieldPolicy, FieldReadFunction, TypePolicies, TypePolicy } from '@apollo/client/cache'; import { FieldPolicy, FieldReadFunction, TypePolicies, TypePolicy } from '@apollo/client/cache';
import { GetChaptersQuery } from "@/lib/graphql/generated/graphql.ts";
export type AboutPayloadKeySpecifier = ('buildTime' | 'buildType' | 'discord' | 'github' | 'name' | 'revision' | 'version' | AboutPayloadKeySpecifier)[]; export type AboutPayloadKeySpecifier = ('buildTime' | 'buildType' | 'discord' | 'github' | 'name' | 'revision' | 'version' | AboutPayloadKeySpecifier)[];
export type AboutPayloadFieldPolicy = { export type AboutPayloadFieldPolicy = {
buildTime?: FieldPolicy<any> | FieldReadFunction<any>, buildTime?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -491,7 +492,7 @@ export type QueryFieldPolicy = {
categories?: FieldPolicy<any> | FieldReadFunction<any>, categories?: FieldPolicy<any> | FieldReadFunction<any>,
category?: FieldPolicy<any> | FieldReadFunction<any>, category?: FieldPolicy<any> | FieldReadFunction<any>,
chapter?: FieldPolicy<any> | FieldReadFunction<any>, chapter?: FieldPolicy<any> | FieldReadFunction<any>,
chapters?: FieldPolicy<any> | FieldReadFunction<any>, chapters?: FieldPolicy<GetChaptersQuery['chapters']> | FieldReadFunction<GetChaptersQuery['chapters']>,
checkForServerUpdates?: FieldPolicy<any> | FieldReadFunction<any>, checkForServerUpdates?: FieldPolicy<any> | FieldReadFunction<any>,
checkForWebUIUpdate?: FieldPolicy<any> | FieldReadFunction<any>, checkForWebUIUpdate?: FieldPolicy<any> | FieldReadFunction<any>,
downloadStatus?: FieldPolicy<any> | FieldReadFunction<any>, downloadStatus?: FieldPolicy<any> | FieldReadFunction<any>,

View File

@@ -2378,7 +2378,7 @@ export type GetSourceMangasFetchMutationVariables = Exact<{
}>; }>;
export type GetSourceMangasFetchMutation = { __typename?: 'Mutation', fetchSourceManga: { __typename?: 'FetchSourceMangaPayload', clientMutationId?: string | null, hasNextPage: boolean, mangas: Array<{ __typename?: 'MangaType', unreadCount: number, downloadCount: number, artist?: string | null, author?: string | null, chaptersLastFetchedAt?: any | null, description?: string | null, genre: Array<string>, id: number, inLibrary: boolean, inLibraryAt: any, initialized: boolean, lastFetchedAt?: any | null, realUrl?: string | null, status: MangaStatus, thumbnailUrl?: string | null, title: string, url: string, lastReadChapter?: { __typename?: 'ChapterType', chapterNumber: number, fetchedAt: any, id: number, isBookmarked: boolean, isDownloaded: boolean, isRead: boolean, lastPageRead: number, lastReadAt: any, name: string, pageCount: number, realUrl?: string | null, scanlator?: string | null, sourceOrder: number, uploadDate: any, url: string, manga: { __typename?: 'MangaType', unreadCount: number, downloadCount: number, artist?: string | null, author?: string | null, chaptersLastFetchedAt?: any | null, description?: string | null, genre: Array<string>, id: number, inLibrary: boolean, inLibraryAt: any, initialized: boolean, lastFetchedAt?: any | null, realUrl?: string | null, status: MangaStatus, thumbnailUrl?: string | null, title: string, url: string, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeInUpdate, name: string, order: number, meta: Array<{ __typename?: 'CategoryMetaType', key: string, value: string }>, mangas: { __typename?: 'MangaNodeList', totalCount: number } }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, meta: Array<{ __typename?: 'MangaMetaType', key: string, value: string }>, source?: { __typename?: 'SourceType', displayName: string, iconUrl: string, id: any, isConfigurable: boolean, isNsfw: boolean, lang: string, name: string, supportsLatest: boolean } | null }, meta: Array<{ __typename?: 'ChapterMetaType', key: string, value: string }> } | null, categories: { __typename?: 'CategoryNodeList', totalCount: number, nodes: Array<{ __typename?: 'CategoryType', default: boolean, id: number, includeInUpdate: IncludeInUpdate, name: string, order: number, meta: Array<{ __typename?: 'CategoryMetaType', key: string, value: string }>, mangas: { __typename?: 'MangaNodeList', totalCount: number } }> }, chapters: { __typename?: 'ChapterNodeList', totalCount: number }, meta: Array<{ __typename?: 'MangaMetaType', key: string, value: string }>, source?: { __typename?: 'SourceType', displayName: string, iconUrl: string, id: any, isConfigurable: boolean, isNsfw: boolean, lang: string, name: string, supportsLatest: boolean } | null }> } }; export type GetSourceMangasFetchMutation = { __typename?: 'Mutation', fetchSourceManga: { __typename?: 'FetchSourceMangaPayload', clientMutationId?: string | null, hasNextPage: boolean, mangas: Array<{ __typename?: 'MangaType', artist?: string | null, author?: string | null, chaptersLastFetchedAt?: any | null, description?: string | null, genre: Array<string>, id: number, inLibrary: boolean, inLibraryAt: any, initialized: boolean, lastFetchedAt?: any | null, realUrl?: string | null, status: MangaStatus, thumbnailUrl?: string | null, title: string, url: string, meta: Array<{ __typename?: 'MangaMetaType', key: string, value: string }>, source?: { __typename?: 'SourceType', displayName: string, iconUrl: string, id: any, isConfigurable: boolean, isNsfw: boolean, lang: string, name: string, supportsLatest: boolean } | null }> } };
export type UpdateSourcePreferencesMutationVariables = Exact<{ export type UpdateSourcePreferencesMutationVariables = Exact<{
input: UpdateSourcePreferenceInput; input: UpdateSourcePreferenceInput;

View File

@@ -11,12 +11,14 @@ import useSWR, { Middleware, SWRConfiguration, SWRResponse } from 'swr';
import useSWRInfinite, { SWRInfiniteConfiguration, SWRInfiniteResponse } from 'swr/infinite'; import useSWRInfinite, { SWRInfiniteConfiguration, SWRInfiniteResponse } from 'swr/infinite';
import { import {
ApolloError, ApolloError,
ApolloQueryResult,
DocumentNode, DocumentNode,
FetchResult, FetchResult,
MutationHookOptions, MutationHookOptions,
MutationOptions, MutationOptions,
MutationTuple, MutationTuple,
QueryHookOptions, QueryHookOptions,
QueryOptions,
QueryResult, QueryResult,
TypedDocumentNode, TypedDocumentNode,
useMutation, useMutation,
@@ -24,12 +26,13 @@ import {
} from '@apollo/client'; } from '@apollo/client';
import { OperationVariables } from '@apollo/client/core'; import { OperationVariables } from '@apollo/client/core';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { BackupValidationResult, IChapter, IMangaChapter, PaginatedList } from '@/typings.ts'; import { BackupValidationResult } from '@/typings.ts';
import { HttpMethod as DefaultHttpMethod, IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts'; import { HttpMethod as DefaultHttpMethod, IRestClient, RestClient } from '@/lib/requests/client/RestClient.ts';
import storage from '@/util/localStorage.tsx'; import storage from '@/util/localStorage.tsx';
import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts'; import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts';
import { import {
CategoryOrderBy, CategoryOrderBy,
ChapterOrderBy,
CheckForServerUpdatesQuery, CheckForServerUpdatesQuery,
CheckForServerUpdatesQueryVariables, CheckForServerUpdatesQueryVariables,
ClearDownloaderMutation, ClearDownloaderMutation,
@@ -62,10 +65,16 @@ import {
GetCategoriesQueryVariables, GetCategoriesQueryVariables,
GetCategoryMangasQuery, GetCategoryMangasQuery,
GetCategoryMangasQueryVariables, GetCategoryMangasQueryVariables,
GetChapterPagesFetchMutation,
GetChapterPagesFetchMutationVariables,
GetChaptersQuery,
GetChaptersQueryVariables,
GetExtensionsQuery, GetExtensionsQuery,
GetExtensionsQueryVariables, GetExtensionsQueryVariables,
GetGlobalMetadatasQuery, GetGlobalMetadatasQuery,
GetGlobalMetadatasQueryVariables, GetGlobalMetadatasQueryVariables,
GetMangaChaptersFetchMutation,
GetMangaChaptersFetchMutationVariables,
GetMangaFetchMutation, GetMangaFetchMutation,
GetMangaFetchMutationVariables, GetMangaFetchMutationVariables,
GetMangaQuery, GetMangaQuery,
@@ -89,6 +98,7 @@ import {
SetGlobalMetadataMutation, SetGlobalMetadataMutation,
SetGlobalMetadataMutationVariables, SetGlobalMetadataMutationVariables,
SetMangaMetadataMutation, SetMangaMetadataMutation,
SortOrder,
SourcePreferenceChangeInput, SourcePreferenceChangeInput,
StartDownloaderMutation, StartDownloaderMutation,
StartDownloaderMutationVariables, StartDownloaderMutationVariables,
@@ -153,7 +163,13 @@ import {
STOP_DOWNLOADER, STOP_DOWNLOADER,
} from '@/lib/graphql/mutations/DownloaderMutation.ts'; } from '@/lib/graphql/mutations/DownloaderMutation.ts';
import { GET_CHAPTER, GET_CHAPTERS } from '@/lib/graphql/queries/ChapterQuery.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'; import {
GET_CHAPTER_PAGES_FETCH,
GET_MANGA_CHAPTERS_FETCH,
SET_CHAPTER_METADATA,
UPDATE_CHAPTER,
UPDATE_CHAPTERS,
} from '@/lib/graphql/mutations/ChapterMutation.ts';
import { import {
CREATE_CATEGORY, CREATE_CATEGORY,
DELETE_CATEGORY, DELETE_CATEGORY,
@@ -178,6 +194,7 @@ enum SWRHttpMethod {
} }
enum GQLMethod { enum GQLMethod {
QUERY = 'QUERY',
USE_QUERY = 'USE_QUERY', USE_QUERY = 'USE_QUERY',
USE_MUTATION = 'USE_MUTATION', USE_MUTATION = 'USE_MUTATION',
MUTATION = 'MUTATION', MUTATION = 'MUTATION',
@@ -186,8 +203,6 @@ enum GQLMethod {
type HttpMethodType = DefaultHttpMethod | SWRHttpMethod; type HttpMethodType = DefaultHttpMethod | SWRHttpMethod;
const HttpMethod = { ...SWRHttpMethod, ...DefaultHttpMethod }; const HttpMethod = { ...SWRHttpMethod, ...DefaultHttpMethod };
type RequestOption = { doOnlineFetch?: boolean };
type CustomSWROptions<Data> = { type CustomSWROptions<Data> = {
skipRequest?: boolean; skipRequest?: boolean;
getEndpoint?: (index: number, previousData: Data | null) => string | null; getEndpoint?: (index: number, previousData: Data | null) => string | null;
@@ -214,6 +229,9 @@ export type AbortableSWRInfiniteResponse<Data = any, Error = any> = SWRInfiniteR
AbortableRequest & AbortableRequest &
SWRInfiniteResponseLoadInfo; SWRInfiniteResponseLoadInfo;
export type AbortabaleApolloQueryResponse<Data = any> = {
response: Promise<ApolloQueryResult<Data>>;
} & AbortableRequest;
export type AbortableApolloUseQueryResponse< export type AbortableApolloUseQueryResponse<
Data = any, Data = any,
Variables extends OperationVariables = OperationVariables, Variables extends OperationVariables = OperationVariables,
@@ -659,6 +677,13 @@ export class RequestManager {
return `${this.getValidUrlFor(imageUrl, apiVersion)}${useCacheQuery}`; return `${this.getValidUrlFor(imageUrl, apiVersion)}${useCacheQuery}`;
} }
private doRequestNew<Data, Variables extends OperationVariables = OperationVariables>(
method: GQLMethod.QUERY,
operation: TypedDocumentNode<Data, Variables>,
variables: Variables,
options?: Partial<QueryOptions<Variables, Data>>,
): AbortabaleApolloQueryResponse<Data>;
private doRequestNew<Data, Variables extends OperationVariables = OperationVariables>( private doRequestNew<Data, Variables extends OperationVariables = OperationVariables>(
method: GQLMethod.USE_QUERY, method: GQLMethod.USE_QUERY,
operation: TypedDocumentNode<Data, Variables>, operation: TypedDocumentNode<Data, Variables>,
@@ -685,15 +710,33 @@ export class RequestManager {
operation: TypedDocumentNode<Data, Variables>, operation: TypedDocumentNode<Data, Variables>,
variables: Variables, variables: Variables,
options?: options?:
| QueryOptions<Variables, Data>
| QueryHookOptions<Data, Variables> | QueryHookOptions<Data, Variables>
| MutationHookOptions<Data, Variables> | MutationHookOptions<Data, Variables>
| MutationOptions<Data, Variables>, | MutationOptions<Data, Variables>,
): ):
| AbortabaleApolloQueryResponse<Data>
| AbortableApolloUseQueryResponse<Data, Variables> | AbortableApolloUseQueryResponse<Data, Variables>
| AbortableApolloUseMutationResponse<Data, Variables> | AbortableApolloUseMutationResponse<Data, Variables>
| AbortableApolloMutationResponse<Data> { | AbortableApolloMutationResponse<Data> {
const { signal, abortRequest } = this.createAbortController(); const { signal, abortRequest } = this.createAbortController();
switch (method) { switch (method) {
case GQLMethod.QUERY:
return {
response: this.graphQLClient.client.query<Data, Variables>({
query: operation,
variables,
...(options as QueryOptions<Variables, Data>),
context: {
...options?.context,
fetchOptions: {
signal,
...options?.context?.fetchOptions,
},
},
}),
abortRequest,
};
case GQLMethod.USE_QUERY: case GQLMethod.USE_QUERY:
return { return {
...useQuery<Data, Variables>(operation, { ...useQuery<Data, Variables>(operation, {
@@ -1302,33 +1345,116 @@ export class RequestManager {
); );
} }
public useGetChapters(
variables: GetChaptersQueryVariables,
options?: QueryHookOptions<GetChaptersQuery, GetChaptersQueryVariables>,
): AbortableApolloUseQueryResponse<GetChaptersQuery, GetChaptersQueryVariables> {
return this.doRequestNew(GQLMethod.USE_QUERY, GET_CHAPTERS, variables, options);
}
public useGetMangaChapters( public useGetMangaChapters(
mangaId: number | string, mangaId: number | string,
{ doOnlineFetch, ...swrOptions }: SWROptions<IChapter[]> & RequestOption = {}, options?: QueryHookOptions<GetChaptersQuery, GetChaptersQueryVariables>,
): AbortableSWRResponse<IChapter[]> { ): AbortableApolloUseQueryResponse<GetChaptersQuery, GetChaptersQueryVariables> {
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : ''; return this.useGetChapters(
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}/chapters${onlineFetch}`, { {
swrOptions, condition: { mangaId: Number(mangaId) },
}); orderBy: ChapterOrderBy.SourceOrder,
orderByType: SortOrder.Desc,
},
options,
);
} }
public getMangaChapters(mangaId: number | string, doOnlineFetch?: boolean): AbortableAxiosResponse<IChapter[]> { public getMangaChaptersFetch(
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : ''; mangaId: number | string,
return this.doRequest(HttpMethod.GET, `manga/${mangaId}/chapters${onlineFetch}`); options?: MutationOptions<GetMangaChaptersFetchMutation, GetMangaChaptersFetchMutationVariables>,
): AbortableApolloMutationResponse<GetMangaChaptersFetchMutation> {
return this.doRequestNew<GetMangaChaptersFetchMutation, GetMangaChaptersFetchMutationVariables>(
GQLMethod.MUTATION,
GET_MANGA_CHAPTERS_FETCH,
{ input: { mangaId: Number(mangaId) } },
{ refetchQueries: [GET_MANGA, GET_MANGAS, GET_CHAPTER, GET_CHAPTERS], ...options },
);
} }
public useGetChapter( public useGetMangaChapter(
mangaId: number | string, mangaId: number | string,
chapterIndex: number | string, chapterIndex: number | string,
swrOptions?: SWROptions<IChapter>, options?: QueryHookOptions<GetChaptersQuery, GetChaptersQueryVariables>,
): AbortableSWRResponse<IChapter> { ): AbortableApolloUseQueryResponse<
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}/chapter/${chapterIndex}`, { Omit<GetChaptersQuery, 'chapters'> & { chapter: GetChaptersQuery['chapters']['nodes'][number] },
swrOptions, GetChaptersQueryVariables
}); > {
type Response = AbortableApolloUseQueryResponse<
Omit<GetChaptersQuery, 'chapters'> & { chapter: GetChaptersQuery['chapters']['nodes'][number] },
GetChaptersQueryVariables
>;
const chapterResponse = this.useGetChapters(
{ condition: { mangaId: Number(mangaId), sourceOrder: Number(chapterIndex) } },
options,
);
if (!chapterResponse.data) {
return chapterResponse as unknown as Response;
}
return {
...chapterResponse,
data: {
chapter: chapterResponse.data.chapters.nodes[0],
},
} as unknown as Response;
} }
public getChapter(mangaId: number | string, chapterIndex: number | string): AbortableAxiosResponse<IChapter> { public getChapter(
return this.doRequest(HttpMethod.GET, `manga/${mangaId}/chapter/${chapterIndex}`); mangaId: number | string,
chapterIndex: number | string,
): AbortabaleApolloQueryResponse<
Omit<GetChaptersQuery, 'chapters'> & { chapter: GetChaptersQuery['chapters']['nodes'][number] }
> {
type ResponseData = Omit<GetChaptersQuery, 'chapters'> & {
chapter: GetChaptersQuery['chapters']['nodes'][number];
};
const chapterRequest = this.doRequestNew<GetChaptersQuery, GetChaptersQueryVariables>(
GQLMethod.QUERY,
GET_CHAPTERS,
{
condition: { mangaId: Number(mangaId), sourceOrder: Number(chapterIndex) },
},
);
return {
...chapterRequest,
response: chapterRequest.response.then((chapterResponse) => {
if (!chapterResponse.data) {
return chapterResponse;
}
return {
...chapterResponse,
data: {
chapter: chapterResponse.data.chapters.nodes[0],
},
};
}) as Promise<ApolloQueryResult<ResponseData>>,
};
}
public useGetChapterPagesFetch(
chapterId: string | number,
options?: MutationHookOptions<GetChapterPagesFetchMutation, GetChapterPagesFetchMutationVariables>,
): AbortableApolloUseMutationResponse<GetChapterPagesFetchMutation, GetChapterPagesFetchMutationVariables> {
return this.doRequestNew(
GQLMethod.USE_MUTATION,
GET_CHAPTER_PAGES_FETCH,
{
input: { chapterId: Number(chapterId) },
},
{ refetchQueries: [GET_CHAPTER, GET_CHAPTERS], ...options },
);
} }
public deleteDownloadedChapter(id: number): AbortableApolloMutationResponse<DeleteDownloadedChapterMutation> { public deleteDownloadedChapter(id: number): AbortableApolloMutationResponse<DeleteDownloadedChapterMutation> {
@@ -1552,17 +1678,36 @@ export class RequestManager {
} }
public useGetRecentlyUpdatedChapters( public useGetRecentlyUpdatedChapters(
initialPages?: number, initialPages: number = 1,
swrOptions?: SWRInfiniteOptions<PaginatedList<IMangaChapter>>, options?: QueryHookOptions<GetChaptersQuery, GetChaptersQueryVariables>,
): AbortableSWRInfiniteResponse<PaginatedList<IMangaChapter>> { ): AbortableApolloUseQueryResponse<GetChaptersQuery, GetChaptersQueryVariables> {
return this.doRequest(HttpMethod.SWR_GET_INFINITE, '', { const PAGE_SIZE = 50;
swrOptions: { const CACHE_KEY = 'useGetRecentlyUpdatedChapters';
getEndpoint: (page, previousData) =>
previousData?.hasNextPage ?? true ? `update/recentChapters/${page}` : null, const offset = this.cache.getResponseFor<number>(CACHE_KEY, undefined) ?? 0;
initialSize: initialPages, const [lastOffset] = useState(offset);
...swrOptions,
} as typeof swrOptions, const result = this.useGetChapters(
}); {
filter: { inLibrary: { equalTo: true } },
orderBy: ChapterOrderBy.FetchedAt,
orderByType: SortOrder.Desc,
first: initialPages * PAGE_SIZE + lastOffset,
},
options,
);
return {
...result,
fetchMore: (...args: Parameters<(typeof result)['fetchMore']>) => {
const fetchMoreOptions = args[0] ?? {};
this.cache.cacheResponse(CACHE_KEY, undefined, fetchMoreOptions.variables?.offset);
return result.fetchMore({
...fetchMoreOptions,
variables: { first: PAGE_SIZE, ...fetchMoreOptions.variables },
});
},
} as typeof result;
} }
public startGlobalUpdate(): AbortableApolloMutationResponse<UpdateLibraryMangasMutation>; public startGlobalUpdate(): AbortableApolloMutationResponse<UpdateLibraryMangasMutation>;

View File

@@ -6,16 +6,44 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { ApolloClient, ApolloClientOptions, InMemoryCache, NormalizedCacheObject } from '@apollo/client'; import { ApolloClient, ApolloClientOptions, InMemoryCache, NormalizedCacheObject, Reference } from '@apollo/client';
import { createUploadLink } from 'apollo-upload-client'; import { createUploadLink } from 'apollo-upload-client';
import { BaseClient } from '@/lib/requests/client/BaseClient.ts'; import { BaseClient } from '@/lib/requests/client/BaseClient.ts';
import { StrictTypedTypePolicies } from '@/lib/graphql/generated/apollo-helpers.ts'; import { StrictTypedTypePolicies } from '@/lib/graphql/generated/apollo-helpers.ts';
/* eslint-disable no-underscore-dangle */
const typePolicies: StrictTypedTypePolicies = { const typePolicies: StrictTypedTypePolicies = {
GlobalMetaType: { keyFields: ['key'] }, GlobalMetaType: { keyFields: ['key'] },
ExtensionType: { keyFields: ['apkName'] }, ExtensionType: { keyFields: ['apkName'] },
AboutPayload: { keyFields: [] }, AboutPayload: { keyFields: [] },
Query: {
fields: {
chapters: {
keyArgs: ['condition', 'filter', 'orderBy', 'orderByType'],
merge(existing, incoming) {
const merged = {
...existing,
...incoming,
nodes: existing?.nodes ?? [],
};
const isRefetch = incoming.nodes.some(
(incomingChapter) =>
existing?.nodes.some(
(existingChapter) =>
(existingChapter as unknown as Reference).__ref ===
(incomingChapter as unknown as Reference).__ref,
),
);
if (!isRefetch) {
merged.nodes = [...(existing?.nodes ?? []), ...incoming.nodes];
}
return merged;
},
},
},
},
}; };
/* eslint-enable no-underscore-dangle */
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export class GraphQLClient extends BaseClient< export class GraphQLClient extends BaseClient<

View File

@@ -97,7 +97,7 @@ const Manga: React.FC = () => {
{isLoading && <LoadingPlaceholder />} {isLoading && <LoadingPlaceholder />}
{manga && <MangaDetails manga={manga} />} {manga && <MangaDetails manga={manga} />}
<ChapterList mangaId={id} /> {manga && <ChapterList manga={manga} isRefreshing={refreshing} />}
</Box> </Box>
); );
}; };

View File

@@ -7,11 +7,11 @@
*/ */
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ChapterOffset, IChapter, IReaderSettings, ReaderType, TranslationKey } from '@/typings'; import { ChapterOffset, IReaderSettings, ReaderType, TranslationKey } from '@/typings';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import { import {
checkAndHandleMissingStoredReaderSettings, checkAndHandleMissingStoredReaderSettings,
@@ -27,12 +27,12 @@ import VerticalPager from '@/components/reader/pager/VerticalPager';
import ReaderNavBar from '@/components/navbar/ReaderNavBar'; import ReaderNavBar from '@/components/navbar/ReaderNavBar';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext from '@/components/context/NavbarContext';
import makeToast from '@/components/util/Toast'; import makeToast from '@/components/util/Toast';
import { MangaType } from '@/lib/graphql/generated/graphql.ts'; import { ChapterType, MangaType } from '@/lib/graphql/generated/graphql.ts';
const isDupChapter = async (chapterIndex: number, currentChapter: IChapter) => { const isDupChapter = async (chapterIndex: number, currentChapter: ChapterType) => {
const nextChapter = await requestManager.getChapter(currentChapter.mangaId, chapterIndex).response; const nextChapter = await requestManager.getChapter(currentChapter.manga.id, chapterIndex).response;
return nextChapter.chapterNumber === currentChapter.chapterNumber; return nextChapter.data.chapter.chapterNumber === currentChapter.chapterNumber;
}; };
/** /**
@@ -42,7 +42,7 @@ const isDupChapter = async (chapterIndex: number, currentChapter: IChapter) => {
*/ */
const getOffsetChapter = async ( const getOffsetChapter = async (
chapterIndex: number, chapterIndex: number,
currentChapter: IChapter, currentChapter: ChapterType,
skipDupChapters: boolean, skipDupChapters: boolean,
offset: ChapterOffset, offset: ChapterOffset,
): Promise<number> => { ): Promise<number> => {
@@ -82,11 +82,11 @@ const getReaderComponent = (readerType: ReaderType) => {
const range = (n: number) => Array.from({ length: n }, (value, key) => key); const range = (n: number) => Array.from({ length: n }, (value, key) => key);
const initialChapter = { const initialChapter = {
pageCount: -1, pageCount: -1,
index: -1, sourceOrder: -1,
chapterCount: 0, chapterCount: 0,
lastPageRead: 0, lastPageRead: 0,
name: 'Loading...', name: 'Loading...',
} as IChapter; } as unknown as ChapterType;
export default function Reader() { export default function Reader() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -104,17 +104,48 @@ export default function Reader() {
genre: [], genre: [],
inLibraryAt: 0, inLibraryAt: 0,
lastReadAt: 0, lastReadAt: 0,
chapters: { totalCount: 0 },
}) as unknown as MangaType, }) as unknown as MangaType,
[mangaId], [mangaId],
); );
const { data, loading: isMangaLoading } = requestManager.useGetManga(mangaId); const { data, loading: isMangaLoading } = requestManager.useGetManga(mangaId);
const loadedChapter = useRef<ChapterType | null>(null);
const isChapterLoaded =
Number(mangaId) === loadedChapter.current?.manga.id &&
Number(chapterIndex) === loadedChapter.current?.sourceOrder &&
loadedChapter.current?.pageCount !== -1;
const manga = (data?.manga as MangaType) ?? initialManga; const manga = (data?.manga as MangaType) ?? initialManga;
const { data: chapter = initialChapter, isLoading: isChapterLoading } = requestManager.useGetChapter( const { data: chapterData, loading: isChapterLoading } = requestManager.useGetMangaChapter(mangaId, chapterIndex, {
mangaId, skip: isChapterLoaded,
chapterIndex, });
{ disableCache: true, revalidateOnFocus: false },
); const getLoadedChapter = () => {
const isAChapterLoaded = loadedChapter.current;
const isSameAsLoadedChapter = isAChapterLoaded && isChapterLoaded;
if (isSameAsLoadedChapter) {
return loadedChapter.current;
}
if (chapterData?.chapter) {
return chapterData.chapter as ChapterType;
}
return null;
};
loadedChapter.current = getLoadedChapter();
const chapter = loadedChapter.current ?? initialChapter;
const [fetchPages, { loading: areChapterPagesLoading }] = requestManager.useGetChapterPagesFetch(chapter.id);
useEffect(() => {
if (!isChapterLoading && chapter.pageCount === -1) {
fetchPages();
}
}, [chapter.id]);
const isLoading = isChapterLoading || areChapterPagesLoading || chapter.pageCount === -1;
const [wasLastPageReadSet, setWasLastPageReadSet] = useState(false); const [wasLastPageReadSet, setWasLastPageReadSet] = useState(false);
const [curPage, setCurPage] = useState<number>(0); const [curPage, setCurPage] = useState<number>(0);
const [pageToScrollTo, setPageToScrollTo] = useState<number | undefined>(undefined); const [pageToScrollTo, setPageToScrollTo] = useState<number | undefined>(undefined);
@@ -136,12 +167,7 @@ export default function Reader() {
setRetrievingNextChapter(true); setRetrievingNextChapter(true);
try { try {
setHistory( setHistory(
await getOffsetChapter( await getOffsetChapter(chapter.sourceOrder + offset, chapter, settings.skipDupChapters, offset),
chapter.index + offset,
chapter as IChapter,
settings.skipDupChapters,
offset,
),
); );
} catch (error) { } catch (error) {
const offsetToTranslationKeyMap: { [chapterOffset in ChapterOffset]: TranslationKey } = { const offsetToTranslationKeyMap: { [chapterOffset in ChapterOffset]: TranslationKey } = {
@@ -158,7 +184,7 @@ export default function Reader() {
); );
useEffect(() => { useEffect(() => {
if (isChapterLoading || !chapter) { if (isLoading || !chapter) {
return; return;
} }
@@ -167,13 +193,13 @@ export default function Reader() {
// last page, also probably read = true, we will load the first page. // last page, also probably read = true, we will load the first page.
setCurPage(0); setCurPage(0);
} else setCurPage(chapter.lastPageRead); } else setCurPage(chapter.lastPageRead);
}, [chapter, isChapterLoading]); }, [chapter, isLoading]);
useEffect(() => { useEffect(() => {
if (!manga?.title || (chapter as IChapter)?.name === t('global.label.loading')) { if (!manga?.title || chapter.name === t('global.label.loading')) {
setTitle(t('reader.title')); setTitle(t('reader.title'));
} else { } else {
setTitle(`${manga.title}: ${(chapter as IChapter).name}`); setTitle(`${manga.title}: ${chapter.name}`);
} }
}, [t, manga, chapter]); }, [t, manga, chapter]);
@@ -193,7 +219,7 @@ export default function Reader() {
settings={settings} settings={settings}
setSettingValue={setSettingValue} setSettingValue={setSettingValue}
manga={manga} manga={manga}
chapter={chapter as IChapter} chapter={chapter}
curPage={curPage} curPage={curPage}
scrollToPage={setPageToScrollTo} scrollToPage={setPageToScrollTo}
openNextChapter={openNextChapter} openNextChapter={openNextChapter}
@@ -212,17 +238,20 @@ 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) { const updateLastPageRead = curPage !== -1;
requestManager.updateChapter(chapter.id, { lastPageRead: curPage }); const updateIsRead = curPage === chapter.pageCount - 1;
} const updateChapter = updateLastPageRead || updateIsRead;
if (curPage === chapter.pageCount - 1) { if (updateChapter) {
requestManager.updateChapter(chapter.id, { isRead: true }); requestManager.updateChapter(chapter.id, {
lastPageRead: updateLastPageRead ? curPage : undefined,
isRead: updateIsRead ? true : undefined,
});
} }
}, [curPage]); }, [curPage]);
const nextChapter = useCallback(() => { const nextChapter = useCallback(() => {
if (chapter.index < chapter.chapterCount) { if (chapter.sourceOrder < manga.chapters.totalCount) {
requestManager.updateChapter(chapter.id, { requestManager.updateChapter(chapter.id, {
lastPageRead: chapter.pageCount - 1, lastPageRead: chapter.pageCount - 1,
isRead: true, isRead: true,
@@ -235,10 +264,10 @@ export default function Reader() {
}), }),
); );
} }
}, [chapter.index, chapter.chapterCount, chapter.pageCount, manga.id, settings.skipDupChapters]); }, [chapter.sourceOrder, manga.chapters.totalCount, chapter.pageCount, manga.id, settings.skipDupChapters]);
const prevChapter = useCallback(() => { const prevChapter = useCallback(() => {
if (chapter.index > 1) { if (chapter.sourceOrder > 1) {
openNextChapter(ChapterOffset.PREV, (prevChapterIndex) => openNextChapter(ChapterOffset.PREV, (prevChapterIndex) =>
navigate(`/manga/${manga.id}/chapter/${prevChapterIndex}`, { navigate(`/manga/${manga.id}/chapter/${prevChapterIndex}`, {
replace: true, replace: true,
@@ -246,7 +275,7 @@ export default function Reader() {
}), }),
); );
} }
}, [chapter.index, manga.id, settings.skipDupChapters]); }, [chapter.sourceOrder, manga.id, settings.skipDupChapters]);
// return spinner while chpater data is loading // return spinner while chpater data is loading
if (chapter.pageCount === -1) { if (chapter.pageCount === -1) {
@@ -283,6 +312,7 @@ export default function Reader() {
> >
<PageNumber settings={settings} curPage={curPage} pageCount={chapter.pageCount} /> <PageNumber settings={settings} curPage={curPage} pageCount={chapter.pageCount} />
<ReaderComponent <ReaderComponent
key={chapter.id}
pages={pages} pages={pages}
pageCount={chapter.pageCount} pageCount={chapter.pageCount}
setCurPage={setCurPage} setCurPage={setCurPage}

View File

@@ -18,12 +18,13 @@ import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next'; import { t as translate } from 'i18next';
import { GroupedVirtuoso } from 'react-virtuoso'; import { GroupedVirtuoso } from 'react-virtuoso';
import { IChapter, IMangaChapter, IQueue } from '@/typings'; import { IQueue } from '@/typings';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import LoadingPlaceholder from '@/components/util/LoadingPlaceholder'; import LoadingPlaceholder from '@/components/util/LoadingPlaceholder';
import EmptyView from '@/components/util/EmptyView'; import EmptyView from '@/components/util/EmptyView';
import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator'; import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext from '@/components/context/NavbarContext';
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
const StyledGroupedVirtuoso = styled(GroupedVirtuoso)(({ theme }) => ({ const StyledGroupedVirtuoso = styled(GroupedVirtuoso)(({ theme }) => ({
// 64px header // 64px header
@@ -81,14 +82,14 @@ function getDateString(date: Date) {
return date.toLocaleDateString(); return date.toLocaleDateString();
} }
const groupByDate = (updates: IMangaChapter[]): [date: string, items: number][] => { const groupByDate = (updates: ChapterType[]): [date: string, items: number][] => {
if (!updates.length) { if (!updates.length) {
return []; return [];
} }
const dateToItemMap = new Map<string, number>(); const dateToItemMap = new Map<string, number>();
updates.forEach((item) => { updates.forEach((item) => {
const date = getDateString(epochToDate(item.chapter.fetchedAt)); const date = getDateString(epochToDate(Number(item.fetchedAt)));
dateToItemMap.set(date, (dateToItemMap.get(date) ?? 0) + 1); dateToItemMap.set(date, (dateToItemMap.get(date) ?? 0) + 1);
}); });
@@ -106,19 +107,18 @@ const Updates: React.FC = () => {
const { setTitle, setAction } = useContext(NavbarContext); const { setTitle, setAction } = useContext(NavbarContext);
const { const {
data: pages = [{ hasNextPage: false, page: [] }], data: chapterUpdateData,
isLoading, loading: isLoading,
size: loadedPages, fetchMore,
setSize: setPages, } = requestManager.useGetRecentlyUpdatedChapters(undefined, {
} = requestManager.useGetRecentlyUpdatedChapters(); fetchPolicy: 'cache-and-network',
const { hasNextPage } = pages[pages.length - 1]; notifyOnNetworkStatusChange: true,
const updateEntries = useMemo( });
() => pages.map((page) => page.page).reduce((lastPageChapters, chapters) => [...lastPageChapters, ...chapters]), const hasNextPage = !!chapterUpdateData?.chapters.pageInfo.hasNextPage;
[pages], const endCursor = chapterUpdateData?.chapters.pageInfo.endCursor;
); const updateEntries = (chapterUpdateData?.chapters.nodes as ChapterType[]) ?? [];
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]);
const [, setWsClient] = useState<WebSocket>(); const [, setWsClient] = useState<WebSocket>();
const [{ queue }, setQueueState] = useState<IQueue>(initialQueue); const [{ queue }, setQueueState] = useState<IQueue>(initialQueue);
@@ -140,12 +140,15 @@ const Updates: React.FC = () => {
setAction(null); setAction(null);
}, [t]); }, [t]);
const downloadForChapter = (chapter: IChapter) => { const downloadForChapter = (chapter: ChapterType) => {
const { index, mangaId } = chapter; const {
return queue.find((q) => index === q.chapterIndex && mangaId === q.mangaId); sourceOrder,
manga: { id: mangaId },
} = chapter;
return queue.find((q) => sourceOrder === q.chapterIndex && mangaId === q.mangaId);
}; };
const downloadChapter = (chapter: IChapter) => { const downloadChapter = (chapter: ChapterType) => {
requestManager.addChapterToDownloadQueue(chapter.id); requestManager.addChapterToDownloadQueue(chapter.id);
}; };
@@ -154,8 +157,8 @@ const Updates: React.FC = () => {
return; return;
} }
setPages(loadedPages + 1); fetchMore({ variables: { offset: updateEntries.length } });
}, [hasNextPage, loadedPages]); }, [hasNextPage, endCursor]);
if (!isLoading && updateEntries.length === 0) { if (!isLoading && updateEntries.length === 0) {
return <EmptyView message={t('updates.error.label.no_updates_available')} />; return <EmptyView message={t('updates.error.label.no_updates_available')} />;
@@ -179,7 +182,8 @@ const Updates: React.FC = () => {
</StyledGroupHeader> </StyledGroupHeader>
)} )}
itemContent={(index) => { itemContent={(index) => {
const { chapter, manga } = updateEntries[index]; const chapter = updateEntries[index];
const { manga } = chapter;
const download = downloadForChapter(chapter); const download = downloadForChapter(chapter);
return ( return (
@@ -187,7 +191,7 @@ const Updates: React.FC = () => {
<Card> <Card>
<CardActionArea <CardActionArea
component={Link} component={Link}
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`} to={`/manga/${chapter.manga.id}/chapter/${chapter.sourceOrder}`}
state={location.state} state={location.state}
> >
<CardContent <CardContent
@@ -208,7 +212,7 @@ const Updates: React.FC = () => {
marginRight: 2, marginRight: 2,
imageRendering: 'pixelated', imageRendering: 'pixelated',
}} }}
src={requestManager.getValidImgUrlFor(manga.thumbnailUrl)} src={requestManager.getValidImgUrlFor(manga.thumbnailUrl ?? '')}
/> />
<Box sx={{ display: 'flex', flexDirection: 'column' }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2"> <Typography variant="h5" component="h2">
@@ -220,7 +224,7 @@ const Updates: React.FC = () => {
</Box> </Box>
</Box> </Box>
{download && <DownloadStateIndicator download={download} />} {download && <DownloadStateIndicator download={download} />}
{download == null && !chapter.downloaded && ( {download == null && !chapter.isDownloaded && (
<IconButton <IconButton
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();

View File

@@ -11,6 +11,7 @@ import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
import { ParseKeys } from 'i18next'; import { ParseKeys } from 'i18next';
import { Location } from 'react-router-dom'; import { Location } from 'react-router-dom';
import { import {
ChapterType,
ExtensionType, ExtensionType,
GetSourceQuery, GetSourceQuery,
MangaType, MangaType,
@@ -165,13 +166,6 @@ export interface IMangaChapter {
chapter: IChapter; chapter: IChapter;
} }
export interface IPartialChapter {
pageCount: number;
index: number;
chapterCount: number;
lastPageRead: number;
}
export enum IncludeInGlobalUpdate { export enum IncludeInGlobalUpdate {
EXCLUDE = 0, EXCLUDE = 0,
INCLUDE = 1, INCLUDE = 1,
@@ -237,7 +231,7 @@ export interface IReaderProps {
initialPage: number; initialPage: number;
settings: IReaderSettings; settings: IReaderSettings;
manga: MangaType; manga: MangaType;
chapter: IChapter | IPartialChapter; chapter: ChapterType;
nextChapter: () => void; nextChapter: () => void;
prevChapter: () => void; prevChapter: () => void;
} }

View File

@@ -9,13 +9,20 @@
import fs from 'fs'; import fs from 'fs';
import * as path from 'path'; import * as path from 'path';
const format = (source: string, regex: RegExp, replaceValue: string): string => source.replace(regex, replaceValue); const format = (source: string, regex: RegExp | string, replaceValue: string): string =>
source.replace(regex, replaceValue);
const generatedGraphQLFilePath = path.resolve(__dirname, '../../src/lib/graphql/generated/graphql.ts'); let generatedGraphQLFilePath = path.resolve(__dirname, '../../src/lib/graphql/generated/graphql.ts');
const generatedGraphQLFile = fs.readFileSync(generatedGraphQLFilePath, 'utf8'); let generatedGraphQLFile = fs.readFileSync(generatedGraphQLFilePath, 'utf8');
// add logic to format the codegen generated graphql file // add logic to format the codegen generated graphql file
/* ******************************************* */
/* */
/* typescript, typescript-operations */
/* */
/* ******************************************* */
const fixCursorTyping = format( const fixCursorTyping = format(
generatedGraphQLFile, generatedGraphQLFile,
/Cursor: \{ input: any; output: any; }/g, /Cursor: \{ input: any; output: any; }/g,
@@ -31,3 +38,73 @@ const fixLongStringTyping = format(
const fixSubscriptionHookNameSuffix = format(fixLongStringTyping, /SubscriptionSubscription/g, 'Subscription'); const fixSubscriptionHookNameSuffix = format(fixLongStringTyping, /SubscriptionSubscription/g, 'Subscription');
fs.writeFileSync(generatedGraphQLFilePath, fixSubscriptionHookNameSuffix); fs.writeFileSync(generatedGraphQLFilePath, fixSubscriptionHookNameSuffix);
/* ****************************************** */
/* */
/* typescript-apollo-client-helpers */
/* */
/* ****************************************** */
generatedGraphQLFilePath = path.resolve(__dirname, '../../src/lib/graphql/generated/apollo-helpers.ts');
generatedGraphQLFile = fs.readFileSync(generatedGraphQLFilePath, 'utf8');
const addImports = format(
generatedGraphQLFile,
`import { FieldPolicy, FieldReadFunction, TypePolicies, TypePolicy } from '@apollo/client/cache';`,
`import { FieldPolicy, FieldReadFunction, TypePolicies, TypePolicy } from '@apollo/client/cache';
import { GetChaptersQuery } from "@/lib/graphql/generated/graphql.ts";`,
);
const fixTypingOfQueryTypePolicies = format(
addImports,
`export type QueryFieldPolicy = {
\tabout?: FieldPolicy<any> | FieldReadFunction<any>,
\tcategories?: FieldPolicy<any> | FieldReadFunction<any>,
\tcategory?: FieldPolicy<any> | FieldReadFunction<any>,
\tchapter?: FieldPolicy<any> | FieldReadFunction<any>,
\tchapters?: FieldPolicy<any> | FieldReadFunction<any>,
\tcheckForServerUpdates?: FieldPolicy<any> | FieldReadFunction<any>,
\tcheckForWebUIUpdate?: FieldPolicy<any> | FieldReadFunction<any>,
\tdownloadStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\textension?: FieldPolicy<any> | FieldReadFunction<any>,
\textensions?: FieldPolicy<any> | FieldReadFunction<any>,
\tgetWebUIUpdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tlastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>,
\tmanga?: FieldPolicy<any> | FieldReadFunction<any>,
\tmangas?: FieldPolicy<any> | FieldReadFunction<any>,
\tmeta?: FieldPolicy<any> | FieldReadFunction<any>,
\tmetas?: FieldPolicy<any> | FieldReadFunction<any>,
\trestoreStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tsettings?: FieldPolicy<any> | FieldReadFunction<any>,
\tsource?: FieldPolicy<any> | FieldReadFunction<any>,
\tsources?: FieldPolicy<any> | FieldReadFunction<any>,
\tupdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tvalidateBackup?: FieldPolicy<any> | FieldReadFunction<any>
};`,
`export type QueryFieldPolicy = {
\tabout?: FieldPolicy<any> | FieldReadFunction<any>,
\tcategories?: FieldPolicy<any> | FieldReadFunction<any>,
\tcategory?: FieldPolicy<any> | FieldReadFunction<any>,
\tchapter?: FieldPolicy<any> | FieldReadFunction<any>,
\tchapters?: FieldPolicy<GetChaptersQuery['chapters']> | FieldReadFunction<GetChaptersQuery['chapters']>,
\tcheckForServerUpdates?: FieldPolicy<any> | FieldReadFunction<any>,
\tcheckForWebUIUpdate?: FieldPolicy<any> | FieldReadFunction<any>,
\tdownloadStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\textension?: FieldPolicy<any> | FieldReadFunction<any>,
\textensions?: FieldPolicy<any> | FieldReadFunction<any>,
\tgetWebUIUpdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tlastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>,
\tmanga?: FieldPolicy<any> | FieldReadFunction<any>,
\tmangas?: FieldPolicy<any> | FieldReadFunction<any>,
\tmeta?: FieldPolicy<any> | FieldReadFunction<any>,
\tmetas?: FieldPolicy<any> | FieldReadFunction<any>,
\trestoreStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tsettings?: FieldPolicy<any> | FieldReadFunction<any>,
\tsource?: FieldPolicy<any> | FieldReadFunction<any>,
\tsources?: FieldPolicy<any> | FieldReadFunction<any>,
\tupdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tvalidateBackup?: FieldPolicy<any> | FieldReadFunction<any>
};`,
);
fs.writeFileSync(generatedGraphQLFilePath, fixTypingOfQueryTypePolicies);