diff --git a/src/components/manga/ChapterCard.tsx b/src/components/manga/ChapterCard.tsx index 1b6962cb..13621bbf 100644 --- a/src/components/manga/ChapterCard.tsx +++ b/src/components/manga/ChapterCard.tsx @@ -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 = (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); @@ -82,12 +73,10 @@ const ChapterCard: React.FC = (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 = (props: IProps) => { }; const deleteChapter = () => { - requestManager.deleteDownloadedChapter(chapter.id).response.then(() => triggerChaptersUpdate()); + requestManager.deleteDownloadedChapter(chapter.id); handleClose(); }; @@ -113,8 +102,8 @@ const ChapterCard: React.FC = (props: IProps) => { } }; - const isDownloaded = chapter.downloaded; - const canBeDownloaded = !chapter.downloaded && dc === undefined; + const { isDownloaded } = chapter; + const canBeDownloaded = !chapter.isDownloaded && dc === undefined; return (
  • @@ -126,9 +115,9 @@ const ChapterCard: React.FC = (props: IProps) => { > @@ -143,7 +132,7 @@ const ChapterCard: React.FC = (props: IProps) => { > - {chapter.bookmarked && ( + {chapter.isBookmarked && ( = (props: IProps) => { {chapter.scanlator} - {getUploadDateString(chapter.uploadDate)} + {getUploadDateString(Number(chapter.uploadDate ?? 0))} {isDownloaded && ` • ${t('chapter.status.label.downloaded')}`} @@ -192,24 +181,24 @@ const ChapterCard: React.FC = (props: IProps) => { {t('chapter.action.download.add.label.action')} )} - sendChange('isBookmarked', !chapter.bookmarked)}> + sendChange('isBookmarked', !chapter.isBookmarked)}> - {chapter.bookmarked && } - {!chapter.bookmarked && } + {chapter.isBookmarked && } + {!chapter.isBookmarked && } - {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')} - sendChange('isRead', !chapter.read)}> + sendChange('isRead', !chapter.isRead)}> - {chapter.read && } - {!chapter.read && } + {chapter.isRead && } + {!chapter.isRead && } - {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')} sendChange('markPrevRead', true)}> diff --git a/src/components/manga/ChapterList.tsx b/src/components/manga/ChapterList.tsx index 21b5b8e4..71e0d134 100644 --- a/src/components/manga/ChapterList.tsx +++ b/src/components/manga/ChapterList.tsx @@ -11,7 +11,7 @@ import Typography from '@mui/material/Typography'; import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { useTranslation } from 'react-i18next'; -import { 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 = ({ mangaId }) => { +const ChapterList: React.FC = ({ manga, isRefreshing }) => { const { t } = useTranslation(); const [selection, setSelection] = useState(null); const prevQueueRef = useRef(); const queue = useSubscription('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 = ({ 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 = ({ 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 ; + } + + if (!isLatestChapterRead) { + return ; + } + + return null; + }, [selectedChapters, isLatestChapterRead]); + + if (isLoading || (noChaptersFound && isRefreshing)) { return (
    = ({ 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 ( <> @@ -273,7 +288,6 @@ const ChapterList: React.FC = ({ mangaId }) => { {...chaptersWithMeta[index]} chapterIds={mangaChapterIds} showChapterNumber={options.showChapterNumber} - triggerChaptersUpdate={() => mutate()} onSelect={() => handleSelection(index)} /> ); @@ -282,11 +296,7 @@ const ChapterList: React.FC = ({ mangaId }) => { overscan={window.innerHeight * 0.5} /> - {selectedChapters !== null ? ( - - ) : ( - firstUnreadChapter && - )} + {chapterListFAB} ); }; diff --git a/src/components/manga/ResumeFAB.tsx b/src/components/manga/ResumeFAB.tsx index 5c43c5d5..6e6c7eba 100644 --- a/src/components/manga/ResumeFAB.tsx +++ b/src/components/manga/ResumeFAB.tsx @@ -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 ( - + - {index === 1 ? t('global.button.start') : t('global.button.resume')} + {chapterIndex === 1 ? t('global.button.start') : t('global.button.resume')} ); } diff --git a/src/components/manga/SelectionFAB.tsx b/src/components/manga/SelectionFAB.tsx index bd2c0de4..93d1e514 100644 --- a/src/components/manga/SelectionFAB.tsx +++ b/src/components/manga/SelectionFAB.tsx @@ -63,38 +63,38 @@ const SelectionFAB: React.FC = (props) => { !c.downloaded && dc === undefined, + ({ chapter: c, downloadChapter: dc }) => !c.isDownloaded && dc === undefined, )} onClick={handleAction} title={t('chapter.action.download.add.button.selected')} /> chapter.downloaded)} + matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isDownloaded)} onClick={handleAction} title={t('chapter.action.download.delete.button.selected')} /> !chapter.bookmarked)} + matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.isBookmarked)} onClick={handleAction} title={t('chapter.action.bookmark.add.button.selected')} /> chapter.bookmarked)} + matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isBookmarked)} onClick={handleAction} title={t('chapter.action.bookmark.remove.button.selected')} /> !chapter.read)} + matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.isRead)} onClick={handleAction} title={t('chapter.action.mark_as_read.add.button.selected')} /> chapter.read)} + matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isRead)} onClick={handleAction} title={t('chapter.action.mark_as_read.remove.button.selected')} /> diff --git a/src/components/manga/hooks.ts b/src/components/manga/hooks.ts index 4327641a..7eb1e41f 100644 --- a/src/components/manga/hooks.ts +++ b/src/components/manga/hooks.ts @@ -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]); diff --git a/src/components/manga/util.tsx b/src/components/manga/util.tsx index 27073b1a..edfb25c0 100644 --- a/src/components/manga/util.tsx +++ b/src/components/manga/util.tsx @@ -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, { read: isChapterRead }: IChapter) { +export function unreadFilter(unread: NullAndUndefined, { isRead: isChapterRead }: ChapterType) { switch (unread) { case true: return !isChapterRead; @@ -57,7 +57,7 @@ export function unreadFilter(unread: NullAndUndefined, { read: isChapte } } -function downloadFilter(downloaded: NullAndUndefined, { downloaded: chapterDownload }: IChapter) { +function downloadFilter(downloaded: NullAndUndefined, { isDownloaded: chapterDownload }: ChapterType) { switch (downloaded) { case true: return chapterDownload; @@ -68,7 +68,7 @@ function downloadFilter(downloaded: NullAndUndefined, { downloaded: cha } } -function bookmarkedFilter(bookmarked: NullAndUndefined, { bookmarked: chapterBookmarked }: IChapter) { +function bookmarkedFilter(bookmarked: NullAndUndefined, { isBookmarked: chapterBookmarked }: ChapterType) { switch (bookmarked) { case true: return chapterBookmarked; @@ -79,7 +79,7 @@ function bookmarkedFilter(bookmarked: NullAndUndefined, { 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( chapterOptionsReducer, `${mangaId}filterOptions`, diff --git a/src/components/navbar/ReaderNavBar.tsx b/src/components/navbar/ReaderNavBar.tsx index b95ef730..236e6474 100644 --- a/src/components/navbar/ReaderNavBar.tsx +++ b/src/components/navbar/ReaderNavBar.tsx @@ -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; - chapter: IChapter; + manga: MangaType; + chapter: ChapterType; curPage: number; scrollToPage: (page: number) => void; openNextChapter: (offset: ChapterOffset, setHistory: (nextChapterIndex: number) => void) => Promise; @@ -304,7 +304,7 @@ export default function ReaderNavBar(props: IProps) { openNextChapter(ChapterOffset.PREV, (prevChapterIndex) => { navigate(`/manga/${manga.id}/chapter/${prevChapterIndex}`, { @@ -322,11 +322,11 @@ export default function ReaderNavBar(props: IProps) {