[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 { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { IChapter, IDownloadChapter } from '@/typings';
import { 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';
import { ChapterType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
interface IProps {
chapter: IChapter;
chapter: ChapterType;
chapterIds: number[];
triggerChaptersUpdate: () => void;
downloadChapter: IDownloadChapter | undefined;
showChapterNumber: boolean;
onSelect: (selected: boolean) => void;
@@ -47,15 +46,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
const { t } = useTranslation();
const theme = useTheme();
const {
chapter,
chapterIds,
triggerChaptersUpdate,
downloadChapter: dc,
showChapterNumber,
onSelect,
selected,
} = props;
const { chapter, chapterIds, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
const isSelecting = selected !== null;
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
@@ -82,12 +73,10 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
return;
}
requestManager
.updateChapter(chapter.id, {
[key]: value,
lastPageRead: key === 'isRead' ? 0 : undefined,
})
.response.then(() => triggerChaptersUpdate());
requestManager.updateChapter(chapter.id, {
[key]: value,
lastPageRead: key === 'isRead' ? 0 : undefined,
});
};
const downloadChapter = () => {
@@ -96,7 +85,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
};
const deleteChapter = () => {
requestManager.deleteDownloadedChapter(chapter.id).response.then(() => triggerChaptersUpdate());
requestManager.deleteDownloadedChapter(chapter.id);
handleClose();
};
@@ -113,8 +102,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
}
};
const isDownloaded = chapter.downloaded;
const canBeDownloaded = !chapter.downloaded && dc === undefined;
const { isDownloaded } = chapter;
const canBeDownloaded = !chapter.isDownloaded && dc === undefined;
return (
<li>
@@ -126,9 +115,9 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
>
<CardActionArea
component={Link}
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
to={`/manga/${chapter.manga.id}/chapter/${chapter.sourceOrder}`}
style={{
color: theme.palette.text[chapter.read ? 'disabled' : 'primary'],
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
}}
onClick={handleClick}
>
@@ -143,7 +132,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
>
<Stack direction="column" flex={1}>
<Typography variant="h5" component="h2">
{chapter.bookmarked && (
{chapter.isBookmarked && (
<BookmarkIcon
color="primary"
sx={{ mr: 0.5, position: 'relative', top: '0.15em' }}
@@ -153,7 +142,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
</Typography>
<Typography variant="caption">{chapter.scanlator}</Typography>
<Typography variant="caption">
{getUploadDateString(chapter.uploadDate)}
{getUploadDateString(Number(chapter.uploadDate ?? 0))}
{isDownloaded && `${t('chapter.status.label.downloaded')}`}
</Typography>
</Stack>
@@ -192,24 +181,24 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
<ListItemText>{t('chapter.action.download.add.label.action')}</ListItemText>
</MenuItem>
)}
<MenuItem onClick={() => sendChange('isBookmarked', !chapter.bookmarked)}>
<MenuItem onClick={() => sendChange('isBookmarked', !chapter.isBookmarked)}>
<ListItemIcon>
{chapter.bookmarked && <BookmarkRemove fontSize="small" />}
{!chapter.bookmarked && <BookmarkAdd fontSize="small" />}
{chapter.isBookmarked && <BookmarkRemove fontSize="small" />}
{!chapter.isBookmarked && <BookmarkAdd fontSize="small" />}
</ListItemIcon>
<ListItemText>
{chapter.bookmarked && t('chapter.action.bookmark.remove.label.action')}
{!chapter.bookmarked && t('chapter.action.bookmark.add.label.action')}
{chapter.isBookmarked && t('chapter.action.bookmark.remove.label.action')}
{!chapter.isBookmarked && t('chapter.action.bookmark.add.label.action')}
</ListItemText>
</MenuItem>
<MenuItem onClick={() => sendChange('isRead', !chapter.read)}>
<MenuItem onClick={() => sendChange('isRead', !chapter.isRead)}>
<ListItemIcon>
{chapter.read && <RemoveDone fontSize="small" />}
{!chapter.read && <Done fontSize="small" />}
{chapter.isRead && <RemoveDone fontSize="small" />}
{!chapter.isRead && <Done fontSize="small" />}
</ListItemIcon>
<ListItemText>
{chapter.read && 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.remove.label.action')}
{!chapter.isRead && t('chapter.action.mark_as_read.add.label.action.current')}
</ListItemText>
</MenuItem>
<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 { Virtuoso } from 'react-virtuoso';
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 useSubscription from '@/components/library/useSubscription';
import ChapterCard from '@/components/manga/ChapterCard';
@@ -22,7 +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';
import { ChapterType, MangaType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
listStyle: 'none',
@@ -69,25 +69,29 @@ const actionsStrings: {
};
export interface IChapterWithMeta {
chapter: IChapter;
chapter: ChapterType;
downloadChapter: IDownloadChapter | undefined;
selected: boolean | null;
}
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 [selection, setSelection] = useState<number[] | null>(null);
const prevQueueRef = useRef<IDownloadChapter[]>();
const queue = useSubscription<IQueue>('downloads').data?.queue;
const [options, dispatch] = useChapterOptions(mangaId);
const { data: chaptersData, mutate, isLoading } = requestManager.useGetMangaChapters(mangaId);
const chapters = useMemo(() => chaptersData ?? [], [chaptersData]);
const [options, dispatch] = useChapterOptions(manga.id);
const { data: chaptersData, loading: isLoading, refetch } = requestManager.useGetMangaChapters(manga.id);
const chapters = useMemo(
() => (chaptersData?.chapters.nodes as ChapterType[]) ?? [],
[chaptersData?.chapters.nodes],
);
const mangaChapterIds = useMemo(() => chapters.map((chapter) => chapter.id), [chapters]);
useEffect(() => {
@@ -102,26 +106,17 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
});
if (changedDownloads.length > 0) {
mutate();
refetch();
}
}
prevQueueRef.current = queue;
}, [queue]);
const visibleChapters = useMemo(
() => filterAndSortChapters(chapters, options), //
[chapters, options],
);
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
const firstUnreadChapter = useMemo(
() =>
chapters
.slice()
.reverse()
.find((chapter) => !chapter.read),
[chapters],
);
const nextChapterIndexToRead = (manga.lastReadChapter?.sourceOrder ?? 0) + 1;
const isLatestChapterRead = manga.chapters.totalCount === manga.lastReadChapter?.sourceOrder;
const handleSelection = (index: number) => {
const chapter = visibleChapters[index];
@@ -174,11 +169,49 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
actionPromise
.then(() => makeToast(t(actionsStrings[action].success, { count: chapterIds.length }), 'success'))
.then(() => mutate())
.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 (
<div
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 (
<>
<Stack direction="column" sx={{ position: 'relative' }}>
@@ -273,7 +288,6 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
{...chaptersWithMeta[index]}
chapterIds={mangaChapterIds}
showChapterNumber={options.showChapterNumber}
triggerChaptersUpdate={() => mutate()}
onSelect={() => handleSelection(index)}
/>
);
@@ -282,11 +296,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
overscan={window.innerHeight * 0.5}
/>
</Stack>
{selectedChapters !== null ? (
<SelectionFAB selectedChapters={selectedChapters} onAction={handleFabAction} />
) : (
firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={mangaId} />
)}
{chapterListFAB}
</>
);
};

View File

@@ -9,25 +9,21 @@
import { Link } from 'react-router-dom';
import { PlayArrow } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import { IChapter } from '@/typings';
import StyledFab from '@/components/util/StyledFab';
interface ResumeFABProps {
chapter: IChapter;
mangaId: string;
chapterIndex: number;
mangaId: number;
}
export default function ResumeFab(props: ResumeFABProps) {
const { t } = useTranslation();
const {
chapter: { index },
mangaId,
} = props;
const { chapterIndex, mangaId } = props;
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 />
{index === 1 ? t('global.button.start') : t('global.button.resume')}
{chapterIndex === 1 ? t('global.button.start') : t('global.button.resume')}
</StyledFab>
);
}

View File

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

View File

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

View File

@@ -11,11 +11,11 @@ import {
ChapterListOptions,
ChapterOptionsReducerAction,
ChapterSortMode,
IChapter,
NullAndUndefined,
TranslationKey,
} from '@/typings';
import { useReducerLocalStorage } from '@/util/useLocalStorage';
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
const defaultChapterOptions: ChapterListOptions = {
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) {
case true:
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) {
case true:
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) {
case true:
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
? chapters.filter(
(chp) =>
@@ -88,14 +88,17 @@ export function filterAndSortChapters(chapters: IChapter[], options: ChapterList
bookmarkedFilter(options.bookmarked, chp),
)
: [...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) {
Sorted.reverse();
}
return Sorted;
}
export const useChapterOptions = (mangaId: string) =>
export const useChapterOptions = (mangaId: number) =>
useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
chapterOptionsReducer,
`${mangaId}filterOptions`,

View File

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

View File

@@ -1,4 +1,5 @@
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 AboutPayloadFieldPolicy = {
buildTime?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -491,7 +492,7 @@ export type QueryFieldPolicy = {
categories?: FieldPolicy<any> | FieldReadFunction<any>,
category?: 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>,
checkForWebUIUpdate?: 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<{
input: UpdateSourcePreferenceInput;

View File

@@ -11,12 +11,14 @@ import useSWR, { Middleware, SWRConfiguration, SWRResponse } from 'swr';
import useSWRInfinite, { SWRInfiniteConfiguration, SWRInfiniteResponse } from 'swr/infinite';
import {
ApolloError,
ApolloQueryResult,
DocumentNode,
FetchResult,
MutationHookOptions,
MutationOptions,
MutationTuple,
QueryHookOptions,
QueryOptions,
QueryResult,
TypedDocumentNode,
useMutation,
@@ -24,12 +26,13 @@ import {
} from '@apollo/client';
import { OperationVariables } from '@apollo/client/core';
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 storage from '@/util/localStorage.tsx';
import { GraphQLClient } from '@/lib/requests/client/GraphQLClient.ts';
import {
CategoryOrderBy,
ChapterOrderBy,
CheckForServerUpdatesQuery,
CheckForServerUpdatesQueryVariables,
ClearDownloaderMutation,
@@ -62,10 +65,16 @@ import {
GetCategoriesQueryVariables,
GetCategoryMangasQuery,
GetCategoryMangasQueryVariables,
GetChapterPagesFetchMutation,
GetChapterPagesFetchMutationVariables,
GetChaptersQuery,
GetChaptersQueryVariables,
GetExtensionsQuery,
GetExtensionsQueryVariables,
GetGlobalMetadatasQuery,
GetGlobalMetadatasQueryVariables,
GetMangaChaptersFetchMutation,
GetMangaChaptersFetchMutationVariables,
GetMangaFetchMutation,
GetMangaFetchMutationVariables,
GetMangaQuery,
@@ -89,6 +98,7 @@ import {
SetGlobalMetadataMutation,
SetGlobalMetadataMutationVariables,
SetMangaMetadataMutation,
SortOrder,
SourcePreferenceChangeInput,
StartDownloaderMutation,
StartDownloaderMutationVariables,
@@ -153,7 +163,13 @@ import {
STOP_DOWNLOADER,
} 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';
import {
GET_CHAPTER_PAGES_FETCH,
GET_MANGA_CHAPTERS_FETCH,
SET_CHAPTER_METADATA,
UPDATE_CHAPTER,
UPDATE_CHAPTERS,
} from '@/lib/graphql/mutations/ChapterMutation.ts';
import {
CREATE_CATEGORY,
DELETE_CATEGORY,
@@ -178,6 +194,7 @@ enum SWRHttpMethod {
}
enum GQLMethod {
QUERY = 'QUERY',
USE_QUERY = 'USE_QUERY',
USE_MUTATION = 'USE_MUTATION',
MUTATION = 'MUTATION',
@@ -186,8 +203,6 @@ enum GQLMethod {
type HttpMethodType = DefaultHttpMethod | SWRHttpMethod;
const HttpMethod = { ...SWRHttpMethod, ...DefaultHttpMethod };
type RequestOption = { doOnlineFetch?: boolean };
type CustomSWROptions<Data> = {
skipRequest?: boolean;
getEndpoint?: (index: number, previousData: Data | null) => string | null;
@@ -214,6 +229,9 @@ export type AbortableSWRInfiniteResponse<Data = any, Error = any> = SWRInfiniteR
AbortableRequest &
SWRInfiniteResponseLoadInfo;
export type AbortabaleApolloQueryResponse<Data = any> = {
response: Promise<ApolloQueryResult<Data>>;
} & AbortableRequest;
export type AbortableApolloUseQueryResponse<
Data = any,
Variables extends OperationVariables = OperationVariables,
@@ -659,6 +677,13 @@ export class RequestManager {
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>(
method: GQLMethod.USE_QUERY,
operation: TypedDocumentNode<Data, Variables>,
@@ -685,15 +710,33 @@ export class RequestManager {
operation: TypedDocumentNode<Data, Variables>,
variables: Variables,
options?:
| QueryOptions<Variables, Data>
| QueryHookOptions<Data, Variables>
| MutationHookOptions<Data, Variables>
| MutationOptions<Data, Variables>,
):
| AbortabaleApolloQueryResponse<Data>
| AbortableApolloUseQueryResponse<Data, Variables>
| AbortableApolloUseMutationResponse<Data, Variables>
| AbortableApolloMutationResponse<Data> {
const { signal, abortRequest } = this.createAbortController();
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:
return {
...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(
mangaId: number | string,
{ doOnlineFetch, ...swrOptions }: SWROptions<IChapter[]> & RequestOption = {},
): AbortableSWRResponse<IChapter[]> {
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : '';
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}/chapters${onlineFetch}`, {
swrOptions,
});
options?: QueryHookOptions<GetChaptersQuery, GetChaptersQueryVariables>,
): AbortableApolloUseQueryResponse<GetChaptersQuery, GetChaptersQueryVariables> {
return this.useGetChapters(
{
condition: { mangaId: Number(mangaId) },
orderBy: ChapterOrderBy.SourceOrder,
orderByType: SortOrder.Desc,
},
options,
);
}
public getMangaChapters(mangaId: number | string, doOnlineFetch?: boolean): AbortableAxiosResponse<IChapter[]> {
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : '';
return this.doRequest(HttpMethod.GET, `manga/${mangaId}/chapters${onlineFetch}`);
public getMangaChaptersFetch(
mangaId: number | string,
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,
chapterIndex: number | string,
swrOptions?: SWROptions<IChapter>,
): AbortableSWRResponse<IChapter> {
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}/chapter/${chapterIndex}`, {
swrOptions,
});
options?: QueryHookOptions<GetChaptersQuery, GetChaptersQueryVariables>,
): AbortableApolloUseQueryResponse<
Omit<GetChaptersQuery, 'chapters'> & { chapter: GetChaptersQuery['chapters']['nodes'][number] },
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> {
return this.doRequest(HttpMethod.GET, `manga/${mangaId}/chapter/${chapterIndex}`);
public getChapter(
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> {
@@ -1552,17 +1678,36 @@ export class RequestManager {
}
public useGetRecentlyUpdatedChapters(
initialPages?: number,
swrOptions?: SWRInfiniteOptions<PaginatedList<IMangaChapter>>,
): AbortableSWRInfiniteResponse<PaginatedList<IMangaChapter>> {
return this.doRequest(HttpMethod.SWR_GET_INFINITE, '', {
swrOptions: {
getEndpoint: (page, previousData) =>
previousData?.hasNextPage ?? true ? `update/recentChapters/${page}` : null,
initialSize: initialPages,
...swrOptions,
} as typeof swrOptions,
});
initialPages: number = 1,
options?: QueryHookOptions<GetChaptersQuery, GetChaptersQueryVariables>,
): AbortableApolloUseQueryResponse<GetChaptersQuery, GetChaptersQueryVariables> {
const PAGE_SIZE = 50;
const CACHE_KEY = 'useGetRecentlyUpdatedChapters';
const offset = this.cache.getResponseFor<number>(CACHE_KEY, undefined) ?? 0;
const [lastOffset] = useState(offset);
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>;

View File

@@ -6,16 +6,44 @@
* 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 { BaseClient } from '@/lib/requests/client/BaseClient.ts';
import { StrictTypedTypePolicies } from '@/lib/graphql/generated/apollo-helpers.ts';
/* eslint-disable no-underscore-dangle */
const typePolicies: StrictTypedTypePolicies = {
GlobalMetaType: { keyFields: ['key'] },
ExtensionType: { keyFields: ['apkName'] },
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
export class GraphQLClient extends BaseClient<

View File

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

View File

@@ -7,11 +7,11 @@
*/
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 { Box } from '@mui/material';
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 {
checkAndHandleMissingStoredReaderSettings,
@@ -27,12 +27,12 @@ import VerticalPager from '@/components/reader/pager/VerticalPager';
import ReaderNavBar from '@/components/navbar/ReaderNavBar';
import NavbarContext from '@/components/context/NavbarContext';
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 nextChapter = await requestManager.getChapter(currentChapter.mangaId, chapterIndex).response;
const isDupChapter = async (chapterIndex: number, currentChapter: ChapterType) => {
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 (
chapterIndex: number,
currentChapter: IChapter,
currentChapter: ChapterType,
skipDupChapters: boolean,
offset: ChapterOffset,
): Promise<number> => {
@@ -82,11 +82,11 @@ const getReaderComponent = (readerType: ReaderType) => {
const range = (n: number) => Array.from({ length: n }, (value, key) => key);
const initialChapter = {
pageCount: -1,
index: -1,
sourceOrder: -1,
chapterCount: 0,
lastPageRead: 0,
name: 'Loading...',
} as IChapter;
} as unknown as ChapterType;
export default function Reader() {
const { t } = useTranslation();
@@ -104,17 +104,48 @@ export default function Reader() {
genre: [],
inLibraryAt: 0,
lastReadAt: 0,
chapters: { totalCount: 0 },
}) as unknown as MangaType,
[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 { data: chapter = initialChapter, isLoading: isChapterLoading } = requestManager.useGetChapter(
mangaId,
chapterIndex,
{ disableCache: true, revalidateOnFocus: false },
);
const { data: chapterData, loading: isChapterLoading } = requestManager.useGetMangaChapter(mangaId, chapterIndex, {
skip: isChapterLoaded,
});
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 [curPage, setCurPage] = useState<number>(0);
const [pageToScrollTo, setPageToScrollTo] = useState<number | undefined>(undefined);
@@ -136,12 +167,7 @@ export default function Reader() {
setRetrievingNextChapter(true);
try {
setHistory(
await getOffsetChapter(
chapter.index + offset,
chapter as IChapter,
settings.skipDupChapters,
offset,
),
await getOffsetChapter(chapter.sourceOrder + offset, chapter, settings.skipDupChapters, offset),
);
} catch (error) {
const offsetToTranslationKeyMap: { [chapterOffset in ChapterOffset]: TranslationKey } = {
@@ -158,7 +184,7 @@ export default function Reader() {
);
useEffect(() => {
if (isChapterLoading || !chapter) {
if (isLoading || !chapter) {
return;
}
@@ -167,13 +193,13 @@ export default function Reader() {
// last page, also probably read = true, we will load the first page.
setCurPage(0);
} else setCurPage(chapter.lastPageRead);
}, [chapter, isChapterLoading]);
}, [chapter, isLoading]);
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'));
} else {
setTitle(`${manga.title}: ${(chapter as IChapter).name}`);
setTitle(`${manga.title}: ${chapter.name}`);
}
}, [t, manga, chapter]);
@@ -193,7 +219,7 @@ export default function Reader() {
settings={settings}
setSettingValue={setSettingValue}
manga={manga}
chapter={chapter as IChapter}
chapter={chapter}
curPage={curPage}
scrollToPage={setPageToScrollTo}
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
if (curPage !== -1) {
requestManager.updateChapter(chapter.id, { lastPageRead: curPage });
}
const updateLastPageRead = curPage !== -1;
const updateIsRead = curPage === chapter.pageCount - 1;
const updateChapter = updateLastPageRead || updateIsRead;
if (curPage === chapter.pageCount - 1) {
requestManager.updateChapter(chapter.id, { isRead: true });
if (updateChapter) {
requestManager.updateChapter(chapter.id, {
lastPageRead: updateLastPageRead ? curPage : undefined,
isRead: updateIsRead ? true : undefined,
});
}
}, [curPage]);
const nextChapter = useCallback(() => {
if (chapter.index < chapter.chapterCount) {
if (chapter.sourceOrder < manga.chapters.totalCount) {
requestManager.updateChapter(chapter.id, {
lastPageRead: chapter.pageCount - 1,
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(() => {
if (chapter.index > 1) {
if (chapter.sourceOrder > 1) {
openNextChapter(ChapterOffset.PREV, (prevChapterIndex) =>
navigate(`/manga/${manga.id}/chapter/${prevChapterIndex}`, {
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
if (chapter.pageCount === -1) {
@@ -283,6 +312,7 @@ export default function Reader() {
>
<PageNumber settings={settings} curPage={curPage} pageCount={chapter.pageCount} />
<ReaderComponent
key={chapter.id}
pages={pages}
pageCount={chapter.pageCount}
setCurPage={setCurPage}

View File

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

View File

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

View File

@@ -9,13 +9,20 @@
import fs from 'fs';
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');
const generatedGraphQLFile = fs.readFileSync(generatedGraphQLFilePath, 'utf8');
let generatedGraphQLFilePath = path.resolve(__dirname, '../../src/lib/graphql/generated/graphql.ts');
let generatedGraphQLFile = fs.readFileSync(generatedGraphQLFilePath, 'utf8');
// add logic to format the codegen generated graphql file
/* ******************************************* */
/* */
/* typescript, typescript-operations */
/* */
/* ******************************************* */
const fixCursorTyping = format(
generatedGraphQLFile,
/Cursor: \{ input: any; output: any; }/g,
@@ -31,3 +38,73 @@ const fixLongStringTyping = format(
const fixSubscriptionHookNameSuffix = format(fixLongStringTyping, /SubscriptionSubscription/g, 'Subscription');
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);