[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) =>