diff --git a/src/components/library/LibraryMangaGrid.tsx b/src/components/library/LibraryMangaGrid.tsx index 321f0ec6..c2534b2c 100644 --- a/src/components/library/LibraryMangaGrid.tsx +++ b/src/components/library/LibraryMangaGrid.tsx @@ -110,12 +110,7 @@ interface LibraryMangaGridProps { message?: string; } -const LibraryMangaGrid: React.FC = ({ - mangas, - isLoading, - message, - lastLibraryUpdate, -}) => { +const LibraryMangaGrid: React.FC = ({ mangas, isLoading, message }) => { const { t } = useTranslation(); const [query] = useQueryParam('query', StringParam); @@ -129,7 +124,7 @@ const LibraryMangaGrid: React.FC sortManga(filteredMangas, options.sorts, options.sortDesc), - [filteredMangas, lastLibraryUpdate, options.sorts, options.sortDesc], + [filteredMangas, options.sorts, options.sortDesc], ); const showFilteredOutMessage = @@ -137,7 +132,7 @@ const LibraryMangaGrid: React.FC { window.scrollTo(0, 0); - }, [filteredMangas]); + }, [query, unread, downloaded]); return ( void; -} +const calcProgress = (status: UpdaterSubscription['updateStatusChanged'] | undefined) => { + if (!status) { + return 0; + } -function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) { + const finishedUpdates = status.failedJobs.mangas.totalCount + status.completeJobs.mangas.totalCount; + const totalMangas = finishedUpdates + status.pendingJobs.mangas.totalCount + status.runningJobs.mangas.totalCount; + + const progress = 100 * (finishedUpdates / totalMangas); + + return Number.isNaN(progress) ? 0 : progress; +}; + +function UpdateChecker() { const { t } = useTranslation(); - const [loading, setLoading] = useState(false); - const [progress, setProgress] = useState(0); + const { data: updaterData } = requestManager.useUpdaterSubscription(); + const status = updaterData?.updateStatusChanged; + + const loading = !!status?.isRunning; + const progress = useMemo( + () => calcProgress(status), + [ + status?.failedJobs.mangas.totalCount, + status?.completeJobs.mangas.totalCount, + status?.pendingJobs.mangas.totalCount, + status?.runningJobs.mangas.totalCount, + ], + ); const onClick = async () => { try { - setLoading(true); - setProgress(0); await requestManager.startGlobalUpdate().response; } catch (e) { makeToast(t('global.error.label.update_failed'), 'error'); - setLoading(false); } }; - useEffect(() => { - const wsc = requestManager.getUpdateWebSocket(); - - // "loading" can't be used since it will be outdated once the state gets changed - // it could be used by adding it as a dependency of "useEffect" but then the socket would - // get closed and connected again every time it changes - let updateStarted = false; - - wsc.onmessage = (e) => { - const { running, mangaStatusMap } = JSON.parse(e.data) as IUpdateStatus; - const { COMPLETE = [], RUNNING = [], PENDING = [] } = mangaStatusMap; - - const currentProgress = 100 * (COMPLETE.length / (COMPLETE.length + RUNNING.length + PENDING.length)); - - const isUpdateFinished = currentProgress === 100; - const ignoreFaultyMessage = !updateStarted && !running && isUpdateFinished; - - // for some reason the server sends 100% completed manga updates when connecting to the - // socket while no update is running - if (ignoreFaultyMessage) { - return; - } - - updateStarted = running; - setLoading(running); - setProgress(Number.isNaN(currentProgress) ? 0 : currentProgress); - - if (isUpdateFinished) { - handleFinishedUpdate(Date.now()); - } - }; - - return () => wsc.close(); - }, []); - return ( {loading ? : } diff --git a/src/components/library/useSubscription.ts b/src/components/library/useSubscription.ts deleted file mode 100644 index 0ddc3a63..00000000 --- a/src/components/library/useSubscription.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useEffect, useState } from 'react'; -import requestManager from '@/lib/requests/RequestManager.ts'; - -const useSubscription = (path: string, callback?: (newValue: T) => boolean | void) => { - const [state, setState] = useState(); - - useEffect(() => { - const wsc = new WebSocket(requestManager.getValidWebSocketUrl(path)); - - wsc.onmessage = (e) => { - const data = JSON.parse(e.data) as T; - if (callback) { - // If callback is specified, only update state if callback returns true - // This is so that useSubscription can be used without causing rerender - if (callback(data) === true) { - setState(data); - } - } else { - setState(data); - } - }; - - return () => wsc.close(); - }, [path]); - - return { data: state }; -}; - -export default useSubscription; diff --git a/src/components/manga/ChapterCard.tsx b/src/components/manga/ChapterCard.tsx index 13621bbf..b415db85 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 { IDownloadChapter } from '@/typings'; import requestManager from '@/lib/requests/RequestManager.ts'; import { getUploadDateString } from '@/util/date'; import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator'; -import { ChapterType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; +import { ChapterType, DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; interface IProps { chapter: ChapterType; chapterIds: number[]; - downloadChapter: IDownloadChapter | undefined; + downloadChapter: DownloadType | undefined; showChapterNumber: boolean; onSelect: (selected: boolean) => void; selected: boolean | null; diff --git a/src/components/manga/ChapterList.tsx b/src/components/manga/ChapterList.tsx index 71e0d134..bb53fb22 100644 --- a/src/components/manga/ChapterList.tsx +++ b/src/components/manga/ChapterList.tsx @@ -11,9 +11,8 @@ 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 { IDownloadChapter, IQueue, TranslationKey } from '@/typings'; +import { TranslationKey } from '@/typings'; import requestManager from '@/lib/requests/RequestManager.ts'; -import useSubscription from '@/components/library/useSubscription'; import ChapterCard from '@/components/manga/ChapterCard'; import ResumeFab from '@/components/manga/ResumeFAB'; import { filterAndSortChapters, useChapterOptions } from '@/components/manga/util'; @@ -22,7 +21,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 { ChapterType, MangaType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; +import { ChapterType, DownloadType, MangaType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts'; const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({ listStyle: 'none', @@ -70,7 +69,7 @@ const actionsStrings: { export interface IChapterWithMeta { chapter: ChapterType; - downloadChapter: IDownloadChapter | undefined; + downloadChapter: DownloadType | undefined; selected: boolean | null; } @@ -83,8 +82,9 @@ const ChapterList: React.FC = ({ manga, isRefreshing }) => { const { t } = useTranslation(); const [selection, setSelection] = useState(null); - const prevQueueRef = useRef(); - const queue = useSubscription('downloads').data?.queue; + const prevQueueRef = useRef(); + const { data: downloaderData } = requestManager.useDownloadSubscription(); + const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? []; const [options, dispatch] = useChapterOptions(manga.id); const { data: chaptersData, loading: isLoading, refetch } = requestManager.useGetMangaChapters(manga.id); @@ -99,13 +99,15 @@ const ChapterList: React.FC = ({ manga, isRefreshing }) => { const prevQueue = prevQueueRef.current; const changedDownloads = queue.filter((cd) => { const prevChapterDownload = prevQueue.find( - (pcd) => cd.chapterIndex === pcd.chapterIndex && cd.mangaId === pcd.mangaId, + (pcd) => + cd.chapter.sourceOrder === pcd.chapter.sourceOrder && + cd.chapter.manga.id === pcd.chapter.manga.id, ); if (!prevChapterDownload) return true; return cd.state !== prevChapterDownload.state; }); - if (changedDownloads.length > 0) { + if (changedDownloads.length > 0 || prevQueue?.length !== queue.length) { refetch(); } } @@ -179,7 +181,7 @@ const ChapterList: React.FC = ({ manga, isRefreshing }) => { () => visibleChapters.map((chapter) => { const downloadChapter = queue?.find( - (cd) => cd.chapterIndex === chapter.sourceOrder && cd.mangaId === chapter.manga.id, + (cd) => cd.chapter.sourceOrder === chapter.sourceOrder && cd.chapter.manga.id === chapter.manga.id, ); const selected = selection?.includes(chapter.id) ?? null; return { diff --git a/src/components/manga/MangaDetails.tsx b/src/components/manga/MangaDetails.tsx index 8b148034..71a67232 100644 --- a/src/components/manga/MangaDetails.tsx +++ b/src/components/manga/MangaDetails.tsx @@ -12,7 +12,6 @@ import PublicIcon from '@mui/icons-material/Public'; import { styled } from '@mui/material/styles'; import React, { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { mutate } from 'swr'; import { t as translate } from 'i18next'; import Button from '@mui/material/Button'; import { ISource } from '@/typings'; @@ -177,17 +176,11 @@ const MangaDetails: React.FC = ({ manga }) => { }, [manga.source]); const addToLibrary = () => { - mutate(`/api/v1/manga/${manga.id}`, { ...manga, inLibrary: true }, { revalidate: false }); - requestManager - .updateManga(manga.id, { inLibrary: true }) - .response.then(() => mutate(`/api/v1/manga/${manga.id}`)); + requestManager.updateManga(manga.id, { inLibrary: true }); }; const removeFromLibrary = () => { - mutate(`/api/v1/manga/${manga.id}`, { ...manga, inLibrary: false }, { revalidate: false }); - requestManager - .updateManga(manga.id, { inLibrary: false }) - .response.then(() => mutate(`/api/v1/manga/${manga.id}`)); + requestManager.updateManga(manga.id, { inLibrary: false }); }; return ( diff --git a/src/components/molecules/DownloadStateIndicator.tsx b/src/components/molecules/DownloadStateIndicator.tsx index e4541227..7bf7e2c0 100644 --- a/src/components/molecules/DownloadStateIndicator.tsx +++ b/src/components/molecules/DownloadStateIndicator.tsx @@ -10,17 +10,18 @@ import { CircularProgress, Box } from '@mui/material'; import Typography from '@mui/material/Typography'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { IDownloadChapter, TranslationKey } from '@/typings'; +import { TranslationKey } from '@/typings'; +import { DownloadState, DownloadType } from '@/lib/graphql/generated/graphql.ts'; interface DownloadStateIndicatorProps { - download: IDownloadChapter; + download: DownloadType; } -const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in IDownloadChapter['state']]: TranslationKey } = { - Downloading: 'download.state.label.downloading', - Error: 'download.state.label.error', - Finished: 'download.state.label.finished', - Queued: 'download.state.label.queued', +const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: TranslationKey } = { + DOWNLOADING: 'download.state.label.downloading', + ERROR: 'download.state.label.error', + FINISHED: 'download.state.label.finished', + QUEUED: 'download.state.label.queued', } as const; const DownloadStateIndicator: React.FC = ({ download }) => { diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 471a10e6..fa96b5ee 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -18,9 +18,12 @@ import { QueryHookOptions as ApolloQueryHookOptions, QueryOptions as ApolloQueryOptions, QueryResult, + SubscriptionHookOptions as ApolloSubscriptionHookOptions, + SubscriptionResult, TypedDocumentNode, useMutation, useQuery, + useSubscription, } from '@apollo/client'; import { OperationVariables } from '@apollo/client/core'; import { useEffect, useRef, useState } from 'react'; @@ -47,6 +50,8 @@ import { DequeueChapterDownloadMutationVariables, DequeueChapterDownloadsMutation, DequeueChapterDownloadsMutationVariables, + DownloadStatusSubscription, + DownloadStatusSubscriptionVariables, EnqueueChapterDownloadMutation, EnqueueChapterDownloadMutationVariables, EnqueueChapterDownloadsMutation, @@ -127,6 +132,8 @@ import { UpdateMangaMutation, UpdateMangaMutationVariables, UpdateMangaPatchInput, + UpdaterSubscription, + UpdaterSubscriptionVariables, UpdateSourcePreferencesMutation, UpdateSourcePreferencesMutationVariables, ValidateBackupQuery, @@ -188,12 +195,15 @@ import { GET_UPDATE_STATUS } from '@/lib/graphql/queries/UpdaterQuery.ts'; import { CustomCache } from '@/lib/requests/CustomCache.ts'; import { RESTORE_BACKUP } from '@/lib/graphql/mutations/BackupMutation.ts'; import { VALIDATE_BACKUP } from '@/lib/graphql/queries/BackupQuery.ts'; +import { DOWNLOAD_STATUS_SUBSCRIPTION } from '@/lib/graphql/subscriptions/DownloaderSubscription.ts'; +import { UPDATER_SUBSCRIPTION } from '@/lib/graphql/subscriptions/UpdaterSubscription.ts'; enum GQLMethod { QUERY = 'QUERY', USE_QUERY = 'USE_QUERY', USE_MUTATION = 'USE_MUTATION', MUTATION = 'MUTATION', + USE_SUBSCRIPTION = 'USE_SUBSCRIPTION', } type CustomApolloOptions = { @@ -209,14 +219,12 @@ type CustomApolloOptions = { */ omitAbortSignal?: boolean; }; -type QueryOptions = ApolloQueryOptions< - Variables, - Data +type QueryOptions = Partial< + ApolloQueryOptions > & CustomApolloOptions; -type QueryHookOptions = ApolloQueryHookOptions< - Data, - Variables +type QueryHookOptions = Partial< + ApolloQueryHookOptions > & CustomApolloOptions; type MutationHookOptions = Partial< @@ -227,10 +235,13 @@ type MutationOptions > & CustomApolloOptions; -type ApolloPaginatedMutationOptions< - Data = any, - Variables extends OperationVariables = OperationVariables, -> = MutationHookOptions & { skipRequest?: boolean }; +type ApolloPaginatedMutationOptions = Partial< + MutationHookOptions +> & { skipRequest?: boolean }; +type SubscriptionHookOptions = Partial< + ApolloSubscriptionHookOptions +> & + Omit & { omitAbortSignal?: never }; type AbortableRequest = { abortRequest: AbortController['abort'] }; @@ -292,22 +303,6 @@ export class RequestManager { return this.restClient.getClient().defaults.baseURL!; } - public getWebSocketBaseUrl(): string { - return this.getBaseUrl().replace('http', 'ws'); - } - - public getValidWebSocketUrl(path: string, apiVersion = RequestManager.API_VERSION): string { - return `${this.getWebSocketBaseUrl()}${apiVersion}${path}`; - } - - public getUpdateWebSocket(): WebSocket { - return new WebSocket(this.getValidWebSocketUrl('update')); - } - - public getDownloadWebSocket(): WebSocket { - return new WebSocket(this.getValidWebSocketUrl('downloads')); - } - public getValidUrlFor(endpoint: string, apiVersion: string = RequestManager.API_VERSION): string { return `${this.getBaseUrl()}${apiVersion}${endpoint}`; } @@ -654,46 +649,55 @@ export class RequestManager { private doRequestNew( method: GQLMethod.QUERY, - operation: TypedDocumentNode, - variables: Variables, - options?: Partial>, + operation: DocumentNode | TypedDocumentNode, + variables: Variables | undefined, + options?: QueryOptions, ): AbortabaleApolloQueryResponse; private doRequestNew( method: GQLMethod.USE_QUERY, - operation: TypedDocumentNode, - variables: Variables, - options?: Partial>, + operation: DocumentNode | TypedDocumentNode, + variables: Variables | undefined, + options?: QueryHookOptions, ): AbortableApolloUseQueryResponse; private doRequestNew( method: GQLMethod.USE_MUTATION, - operation: TypedDocumentNode, + operation: DocumentNode | TypedDocumentNode, variables: Variables | undefined, - options?: Partial>, + options?: MutationHookOptions, ): AbortableApolloUseMutationResponse; private doRequestNew( method: GQLMethod.MUTATION, - operation: TypedDocumentNode, - variables: Variables, - options?: Partial>, + operation: DocumentNode | TypedDocumentNode, + variables: Variables | undefined, + options?: MutationOptions, ): AbortableApolloMutationResponse; + private doRequestNew( + method: GQLMethod.USE_SUBSCRIPTION, + operation: DocumentNode | TypedDocumentNode, + variables: Variables | undefined, + options?: SubscriptionHookOptions, + ): SubscriptionResult; + private doRequestNew( method: GQLMethod, - operation: TypedDocumentNode, - variables: Variables, + operation: DocumentNode | TypedDocumentNode, + variables: Variables | undefined, options?: | QueryOptions | QueryHookOptions | MutationHookOptions - | MutationOptions, + | MutationOptions + | SubscriptionHookOptions, ): | AbortabaleApolloQueryResponse | AbortableApolloUseQueryResponse | AbortableApolloUseMutationResponse - | AbortableApolloMutationResponse { + | AbortableApolloMutationResponse + | SubscriptionResult { const { signal, abortRequest } = this.createAbortController(); switch (method) { case GQLMethod.QUERY: @@ -760,6 +764,12 @@ export class RequestManager { }), abortRequest, }; + case GQLMethod.USE_SUBSCRIPTION: + return useSubscription(operation, { + client: this.graphQLClient.client, + variables, + ...(options as SubscriptionHookOptions), + }); default: throw new Error(`unexpected GQLRequest type "${method}"`); } @@ -1566,6 +1576,18 @@ export class RequestManager { ): AbortableApolloUseQueryResponse { return this.doRequestNew(GQLMethod.USE_QUERY, GET_UPDATE_STATUS, {}, options); } + + public useDownloadSubscription( + options?: SubscriptionHookOptions, + ): SubscriptionResult { + return this.doRequestNew(GQLMethod.USE_SUBSCRIPTION, DOWNLOAD_STATUS_SUBSCRIPTION, {}, options); + } + + public useUpdaterSubscription( + options?: SubscriptionHookOptions, + ): SubscriptionResult { + return this.doRequestNew(GQLMethod.USE_SUBSCRIPTION, UPDATER_SUBSCRIPTION, {}, options); + } } const requestManager = new RequestManager(); diff --git a/src/screens/DownloadQueue.tsx b/src/screens/DownloadQueue.tsx index 219b4c41..a913c58b 100644 --- a/src/screens/DownloadQueue.tsx +++ b/src/screens/DownloadQueue.tsx @@ -18,31 +18,26 @@ import { DragDropContext, Draggable } from 'react-beautiful-dnd'; import Typography from '@mui/material/Typography'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { IChapter, IQueue } from '@/typings'; import requestManager from '@/lib/requests/RequestManager.ts'; import StrictModeDroppable from '@/lib/StrictModeDroppable'; import makeToast from '@/components/util/Toast'; import { NavbarToolbar } from '@/components/navbar/DefaultNavBar'; import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator'; -import useSubscription from '@/components/library/useSubscription'; import EmptyView from '@/components/util/EmptyView'; import NavbarContext from '@/components/context/NavbarContext'; - -const initialQueue = { - status: 'Stopped', - queue: [], -} as IQueue; +import { ChapterType, DownloadType } from '@/lib/graphql/generated/graphql.ts'; const DownloadQueue: React.FC = () => { const { t } = useTranslation(); - const { data: queueState } = useSubscription('downloads'); - const { queue, status } = queueState ?? initialQueue; + const { data: downloaderData } = requestManager.useDownloadSubscription(); + const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? []; + const status = downloaderData?.downloadChanged.state ?? 'STARTED'; const { setTitle, setAction } = useContext(NavbarContext); const toggleQueueStatus = () => { - if (status === 'Stopped') { + if (status === 'STOPPED') { requestManager.startDownloads(); } else { requestManager.stopDownloads(); @@ -60,8 +55,8 @@ const DownloadQueue: React.FC = () => { return ; } - const handleDelete = async (chapter: IChapter) => { - const isRunning = status === 'Started'; + const handleDelete = async (chapter: ChapterType) => { + const isRunning = status === 'STARTED'; try { if (isRunning) { @@ -91,7 +86,7 @@ const DownloadQueue: React.FC = () => { <> - {status === 'Stopped' ? : } + {status === 'STOPPED' ? : } @@ -100,8 +95,8 @@ const DownloadQueue: React.FC = () => { {queue.map((item, index) => ( {(draggableProvided, snapshot) => ( @@ -129,7 +124,7 @@ const DownloadQueue: React.FC = () => { - {item.manga.title} + {item.chapter.manga.title} {item.chapter.name} diff --git a/src/screens/Library.tsx b/src/screens/Library.tsx index dd11d669..4b708fb2 100644 --- a/src/screens/Library.tsx +++ b/src/screens/Library.tsx @@ -7,7 +7,7 @@ */ import { Chip, Tab, Tabs, styled, Box } from '@mui/material'; -import React, { useContext, useEffect, useMemo, useState } from 'react'; +import React, { useContext, useEffect, useMemo, useRef } from 'react'; import { useQueryParam, NumberParam } from 'use-query-params'; import { useTranslation } from 'react-i18next'; import requestManager from '@/lib/requests/RequestManager.ts'; @@ -63,7 +63,6 @@ export default function Library() { const { t } = useTranslation(); const { options } = useLibraryOptionsContext(); - const [lastLibraryUpdate, setLastLibraryUpdate] = useState(Date.now()); const { data: categoriesResponse, error: tabsError, @@ -85,7 +84,7 @@ export default function Library() { data: categoryMangaResponse, error: mangaError, loading: mangaLoading, - } = requestManager.useGetCategoryMangas(activeTab?.id, { skip: !activeTab }); + } = requestManager.useGetCategoryMangas(activeTab?.id, { skip: !activeTab, nextFetchPolicy: 'cache-only' }); const mangas = (categoryMangaResponse?.category.mangas.nodes as unknown as MangaType[]) ?? []; const { setTitle, setAction } = useContext(NavbarContext); @@ -102,7 +101,7 @@ export default function Library() { <> - + , ); return () => { @@ -136,7 +135,6 @@ export default function Library() { return ( @@ -183,7 +181,6 @@ export default function Library() { ) : ( diff --git a/src/screens/Updates.tsx b/src/screens/Updates.tsx index 6f0b95b6..12c98818 100644 --- a/src/screens/Updates.tsx +++ b/src/screens/Updates.tsx @@ -13,18 +13,17 @@ import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; -import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo } from 'react'; import { Link, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { t as translate } from 'i18next'; import { GroupedVirtuoso } from 'react-virtuoso'; -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'; +import { ChapterType, DownloadType } from '@/lib/graphql/generated/graphql.ts'; const StyledGroupedVirtuoso = styled(GroupedVirtuoso)(({ theme }) => ({ // 64px header @@ -96,11 +95,6 @@ const groupByDate = (updates: ChapterType[]): [date: string, items: number][] => return [...dateToItemMap.entries()]; }; -const initialQueue = { - status: 'Stopped', - queue: [], -} as IQueue; - const Updates: React.FC = () => { const { t } = useTranslation(); const location = useLocation(); @@ -120,20 +114,8 @@ const Updates: React.FC = () => { 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(); - const [{ queue }, setQueueState] = useState(initialQueue); - - useEffect(() => { - const wsc = requestManager.getDownloadWebSocket(); - wsc.onmessage = (e) => { - const data = JSON.parse(e.data) as IQueue; - setQueueState(data); - }; - - setWsClient(wsc); - - return () => wsc.close(); - }, []); + const { data: downloaderData } = requestManager.useDownloadSubscription(); + const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? []; useEffect(() => { setTitle(t('updates.title')); @@ -146,7 +128,7 @@ const Updates: React.FC = () => { sourceOrder, manga: { id: mangaId }, } = chapter; - return queue.find((q) => sourceOrder === q.chapterIndex && mangaId === q.mangaId); + return queue.find((q) => sourceOrder === q.chapter.sourceOrder && mangaId === q.chapter.manga.id); }; const downloadChapter = (chapter: ChapterType) => {