Use gql for "mangas" IV - source mangas popular/latest
This commit is contained in:
40
src/lib/requests/CustomCache.ts
Normal file
40
src/lib/requests/CustomCache.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export class CustomCache {
|
||||||
|
private keyToResponseMap = new Map<string, unknown>();
|
||||||
|
|
||||||
|
private keyToFetchTimestampMap = new Map<string, number>();
|
||||||
|
|
||||||
|
public readonly createKeyFn = (endpoint: string, data: unknown): string => `${endpoint}_${JSON.stringify(data)}`;
|
||||||
|
|
||||||
|
constructor(createKeyFn?: (endpoint: string, data: unknown) => string) {
|
||||||
|
this.createKeyFn = createKeyFn ?? this.createKeyFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getKeyFor(key: string, data: unknown): string {
|
||||||
|
return this.createKeyFn(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cacheResponse(endpoint: string, data: unknown, response: unknown) {
|
||||||
|
const createdKey = this.getKeyFor(endpoint, data);
|
||||||
|
this.keyToFetchTimestampMap.set(createdKey, Date.now());
|
||||||
|
this.keyToResponseMap.set(createdKey, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFetchTimestampFor(endpoint: string, data: unknown): number | undefined {
|
||||||
|
const key = this.getKeyFor(endpoint, data);
|
||||||
|
return this.keyToFetchTimestampMap.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getResponseFor<Response = any>(endpoint: string, data: unknown): Response | undefined {
|
||||||
|
const key = this.getKeyFor(endpoint, data);
|
||||||
|
return this.keyToResponseMap.get(key) as Response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|||||||
import useSWR, { Middleware, SWRConfiguration, SWRResponse } from 'swr';
|
import useSWR, { Middleware, SWRConfiguration, SWRResponse } from 'swr';
|
||||||
import useSWRInfinite, { SWRInfiniteConfiguration, SWRInfiniteResponse } from 'swr/infinite';
|
import useSWRInfinite, { SWRInfiniteConfiguration, SWRInfiniteResponse } from 'swr/infinite';
|
||||||
import {
|
import {
|
||||||
|
ApolloError,
|
||||||
|
DocumentNode,
|
||||||
FetchResult,
|
FetchResult,
|
||||||
MutationHookOptions,
|
MutationHookOptions,
|
||||||
MutationOptions,
|
MutationOptions,
|
||||||
@@ -21,7 +23,7 @@ import {
|
|||||||
useQuery,
|
useQuery,
|
||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
import { OperationVariables } from '@apollo/client/core';
|
import { OperationVariables } from '@apollo/client/core';
|
||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
BackupValidationResult,
|
BackupValidationResult,
|
||||||
ICategory,
|
ICategory,
|
||||||
@@ -29,7 +31,6 @@ import {
|
|||||||
IMangaChapter,
|
IMangaChapter,
|
||||||
ISourceFilters,
|
ISourceFilters,
|
||||||
PaginatedList,
|
PaginatedList,
|
||||||
PaginatedMangaList,
|
|
||||||
SourcePreferences,
|
SourcePreferences,
|
||||||
SourceSearchResult,
|
SourceSearchResult,
|
||||||
} from '@/typings.ts';
|
} from '@/typings.ts';
|
||||||
@@ -173,6 +174,7 @@ import {
|
|||||||
UPDATE_LIBRARY_MANGAS,
|
UPDATE_LIBRARY_MANGAS,
|
||||||
} from '@/lib/graphql/mutations/UpdaterMutation.ts';
|
} from '@/lib/graphql/mutations/UpdaterMutation.ts';
|
||||||
import { GET_UPDATE_STATUS } from '@/lib/graphql/queries/UpdaterQuery.ts';
|
import { GET_UPDATE_STATUS } from '@/lib/graphql/queries/UpdaterQuery.ts';
|
||||||
|
import { CustomCache } from '@/lib/requests/CustomCache.ts';
|
||||||
|
|
||||||
enum SWRHttpMethod {
|
enum SWRHttpMethod {
|
||||||
SWR_GET,
|
SWR_GET,
|
||||||
@@ -205,6 +207,12 @@ type SWRInfiniteResponseLoadInfo = {
|
|||||||
isInitialLoad: boolean;
|
isInitialLoad: boolean;
|
||||||
isLoadMore: boolean;
|
isLoadMore: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ApolloPaginatedMutationOptions<Data = any, Variables = OperationVariables> = MutationHookOptions<
|
||||||
|
Data,
|
||||||
|
Variables
|
||||||
|
> & { skipRequest?: boolean };
|
||||||
|
|
||||||
type AbortableRequest = { abortRequest: AbortController['abort'] };
|
type AbortableRequest = { abortRequest: AbortController['abort'] };
|
||||||
export type AbortableAxiosResponse<Data = any> = { response: Promise<Data> } & AbortableRequest;
|
export type AbortableAxiosResponse<Data = any> = { response: Promise<Data> } & AbortableRequest;
|
||||||
export type AbortableSWRResponse<Data = any, Error = any> = SWRResponse<Data, Error> & AbortableRequest;
|
export type AbortableSWRResponse<Data = any, Error = any> = SWRResponse<Data, Error> & AbortableRequest;
|
||||||
@@ -212,22 +220,38 @@ export type AbortableSWRInfiniteResponse<Data = any, Error = any> = SWRInfiniteR
|
|||||||
AbortableRequest &
|
AbortableRequest &
|
||||||
SWRInfiniteResponseLoadInfo;
|
SWRInfiniteResponseLoadInfo;
|
||||||
|
|
||||||
type AbortableApolloUseQueryResponse<
|
export type AbortableApolloUseQueryResponse<
|
||||||
Data = any,
|
Data = any,
|
||||||
Variables extends OperationVariables = OperationVariables,
|
Variables extends OperationVariables = OperationVariables,
|
||||||
> = QueryResult<Data, Variables> & AbortableRequest;
|
> = QueryResult<Data, Variables> & AbortableRequest;
|
||||||
type AbortableApolloUseMutationResponse<Data = any, Variables extends OperationVariables = OperationVariables> = [
|
export type AbortableApolloUseMutationResponse<
|
||||||
MutationTuple<Data, Variables>[0],
|
Data = any,
|
||||||
MutationTuple<Data, Variables>[1] & AbortableRequest,
|
Variables extends OperationVariables = OperationVariables,
|
||||||
];
|
> = [MutationTuple<Data, Variables>[0], MutationTuple<Data, Variables>[1] & AbortableRequest];
|
||||||
type AbortableApolloUseMutationPaginatedResponse<
|
export type AbortableApolloUseMutationPaginatedResponse<
|
||||||
Data = any,
|
Data = any,
|
||||||
Variables extends OperationVariables = OperationVariables,
|
Variables extends OperationVariables = OperationVariables,
|
||||||
> = [
|
> = [
|
||||||
(page: number) => Promise<FetchResult<Data>>,
|
(page: number) => Promise<FetchResult<Data>>,
|
||||||
(MutationTuple<Data, Variables>[1] & AbortableRequest & { size: number; loadingMore: boolean })[],
|
(Omit<MutationTuple<Data, Variables>[1], 'loading'> &
|
||||||
|
AbortableRequest & {
|
||||||
|
size: number;
|
||||||
|
/**
|
||||||
|
* Indicates whether any request is currently active.
|
||||||
|
* In case only "isLoading" is true, it means that it's the initial request
|
||||||
|
*/
|
||||||
|
isLoading: boolean;
|
||||||
|
/**
|
||||||
|
* Indicates if a next page is being fetched, which is not part of the initial pages
|
||||||
|
*/
|
||||||
|
isLoadingMore: boolean;
|
||||||
|
/**
|
||||||
|
* Indicates if the cached pages are currently getting revalidated
|
||||||
|
*/
|
||||||
|
isValidating: boolean;
|
||||||
|
})[],
|
||||||
];
|
];
|
||||||
type AbortableApolloMutationResponse<Data = any> = { response: Promise<FetchResult<Data>> } & AbortableRequest;
|
export type AbortableApolloMutationResponse<Data = any> = { response: Promise<FetchResult<Data>> } & AbortableRequest;
|
||||||
|
|
||||||
const isLoadingMore = (swrResult: SWRInfiniteResponse): boolean => {
|
const isLoadingMore = (swrResult: SWRInfiniteResponse): boolean => {
|
||||||
const isNextPageMissing = !!swrResult.data && typeof swrResult.data[swrResult.size - 1] === 'undefined';
|
const isNextPageMissing = !!swrResult.data && typeof swrResult.data[swrResult.size - 1] === 'undefined';
|
||||||
@@ -266,6 +290,8 @@ export class RequestManager {
|
|||||||
|
|
||||||
private readonly restClient: RestClient = new RestClient();
|
private readonly restClient: RestClient = new RestClient();
|
||||||
|
|
||||||
|
private readonly cache = new CustomCache();
|
||||||
|
|
||||||
public getClient(): IRestClient {
|
public getClient(): IRestClient {
|
||||||
return this.restClient;
|
return this.restClient;
|
||||||
}
|
}
|
||||||
@@ -299,6 +325,339 @@ export class RequestManager {
|
|||||||
return `${this.getBaseUrl()}${apiVersion}${endpoint}`;
|
return `${this.getBaseUrl()}${apiVersion}${endpoint}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createAbortController(): { signal: AbortSignal } & AbortableRequest {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const abortRequest = (reason?: any): void => {
|
||||||
|
if (!abortController.signal.aborted) {
|
||||||
|
abortController.abort(reason);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { signal: abortController.signal, abortRequest };
|
||||||
|
}
|
||||||
|
|
||||||
|
private createPaginatedResult<Result extends AbortableApolloUseMutationPaginatedResponse[1][number]>(
|
||||||
|
result: Partial<Result> | undefined | null,
|
||||||
|
defaultPage: number,
|
||||||
|
page?: number,
|
||||||
|
): Result {
|
||||||
|
const isLoading = !result?.error && (result?.isLoading || !result?.called);
|
||||||
|
const size = page ?? result?.size ?? defaultPage;
|
||||||
|
return {
|
||||||
|
client: this.graphQLClient.client,
|
||||||
|
abortRequest: () => {},
|
||||||
|
reset: () => {},
|
||||||
|
called: false,
|
||||||
|
data: undefined,
|
||||||
|
error: undefined,
|
||||||
|
size,
|
||||||
|
isLoading,
|
||||||
|
isLoadingMore: isLoading && size > 1,
|
||||||
|
isValidating: !!result?.isValidating,
|
||||||
|
...result,
|
||||||
|
} as Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async revalidatePage<Data = any, Variables extends OperationVariables = OperationVariables>(
|
||||||
|
cacheResultsKey: string,
|
||||||
|
cachePagesKey: string,
|
||||||
|
getVariablesFor: (page: number) => Variables,
|
||||||
|
options: ApolloPaginatedMutationOptions<Data, Variables> | undefined,
|
||||||
|
checkIfCachedPageIsInvalid: (
|
||||||
|
cachedResult: AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number] | undefined,
|
||||||
|
revalidatedResult: FetchResult<Data>,
|
||||||
|
) => boolean,
|
||||||
|
hasNextPage: (revalidatedResult: FetchResult<Data>) => boolean,
|
||||||
|
pageToRevalidate: number,
|
||||||
|
maxPage: number,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
|
const { response: revalidationRequest } = this.doRequestNew(
|
||||||
|
GQLMethod.MUTATION,
|
||||||
|
GET_SOURCE_MANGAS_FETCH,
|
||||||
|
getVariablesFor(pageToRevalidate),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
context: { fetchOptions: { signal } },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const revalidationResponse = await revalidationRequest;
|
||||||
|
const cachedPageData = this.cache.getResponseFor<
|
||||||
|
AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number]
|
||||||
|
>(cacheResultsKey, getVariablesFor(pageToRevalidate));
|
||||||
|
|
||||||
|
const isCachedPageInvalid = checkIfCachedPageIsInvalid(cachedPageData, revalidationResponse);
|
||||||
|
if (isCachedPageInvalid) {
|
||||||
|
this.cache.cacheResponse(cacheResultsKey, getVariablesFor(pageToRevalidate), revalidationResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasNextPage(revalidationResponse)) {
|
||||||
|
const currentCachedPages = this.cache.getResponseFor<Set<number>>(cachePagesKey, getVariablesFor(0))!;
|
||||||
|
this.cache.cacheResponse(
|
||||||
|
cachePagesKey,
|
||||||
|
getVariablesFor(0),
|
||||||
|
[...currentCachedPages].filter((cachedPage) => cachedPage <= pageToRevalidate),
|
||||||
|
);
|
||||||
|
[...currentCachedPages]
|
||||||
|
.filter((cachedPage) => cachedPage > pageToRevalidate)
|
||||||
|
.forEach((cachedPage) =>
|
||||||
|
this.cache.cacheResponse(cacheResultsKey, getVariablesFor(cachedPage), undefined),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCachedPageInvalid && pageToRevalidate < maxPage) {
|
||||||
|
await this.revalidatePage(
|
||||||
|
cacheResultsKey,
|
||||||
|
cachePagesKey,
|
||||||
|
getVariablesFor,
|
||||||
|
options,
|
||||||
|
checkIfCachedPageIsInvalid,
|
||||||
|
hasNextPage,
|
||||||
|
pageToRevalidate + 1,
|
||||||
|
maxPage,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async revalidatePages<Variables extends OperationVariables = OperationVariables>(
|
||||||
|
activeRevalidationRef:
|
||||||
|
| [ForInput: Variables, Request: Promise<unknown>, AbortRequest: AbortableRequest['abortRequest']]
|
||||||
|
| null,
|
||||||
|
setRevalidationDone: (isDone: boolean) => void,
|
||||||
|
setActiveRevalidation: (
|
||||||
|
activeRevalidation:
|
||||||
|
| [ForInput: Variables, Request: Promise<unknown>, AbortRequest: AbortableRequest['abortRequest']]
|
||||||
|
| null,
|
||||||
|
) => void,
|
||||||
|
getVariablesFor: (page: number) => Variables,
|
||||||
|
setValidating: (isValidating: boolean) => void,
|
||||||
|
revalidatePage: (pageToRevalidate: number, maxPage: number, signal: AbortSignal) => Promise<void>,
|
||||||
|
maxPage: number,
|
||||||
|
abortRequest: AbortableRequest['abortRequest'],
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
|
setRevalidationDone(true);
|
||||||
|
|
||||||
|
const [currRevVars, currRevPromise, currRevAbortRequest] = activeRevalidationRef ?? [];
|
||||||
|
|
||||||
|
const isActiveRevalidationForInput = JSON.stringify(currRevVars) === JSON.stringify(getVariablesFor(0));
|
||||||
|
|
||||||
|
setValidating(true);
|
||||||
|
|
||||||
|
if (!isActiveRevalidationForInput) {
|
||||||
|
currRevAbortRequest?.(new Error('Abort revalidation for different input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let revalidationPromise = currRevPromise;
|
||||||
|
if (!isActiveRevalidationForInput) {
|
||||||
|
revalidationPromise = revalidatePage(1, maxPage, signal);
|
||||||
|
setActiveRevalidation([getVariablesFor(0), revalidationPromise, abortRequest]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await revalidationPromise;
|
||||||
|
setActiveRevalidation(null);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
setValidating(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchPaginatedMutationPage<
|
||||||
|
Data = any,
|
||||||
|
Variables extends OperationVariables = OperationVariables,
|
||||||
|
ResultIdInfo extends Record<string, any> = any,
|
||||||
|
>(
|
||||||
|
getVariablesFor: (page: number) => Variables,
|
||||||
|
setAbortRequest: (abortRequest: AbortableRequest['abortRequest']) => void,
|
||||||
|
getResultIdInfo: () => ResultIdInfo,
|
||||||
|
createPaginatedResult: (
|
||||||
|
result: Partial<AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number]>,
|
||||||
|
) => AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number],
|
||||||
|
setResult: (
|
||||||
|
result: AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number] & ResultIdInfo,
|
||||||
|
) => void,
|
||||||
|
revalidate: (
|
||||||
|
maxPage: number,
|
||||||
|
abortRequest: AbortableRequest['abortRequest'],
|
||||||
|
signal: AbortSignal,
|
||||||
|
) => Promise<void>,
|
||||||
|
options: ApolloPaginatedMutationOptions<Data, Variables> | undefined,
|
||||||
|
documentNode: DocumentNode,
|
||||||
|
cachePagesKey: string,
|
||||||
|
cacheResultsKey: string,
|
||||||
|
cachedPages: Set<number>,
|
||||||
|
newPage: number,
|
||||||
|
): Promise<FetchResult<Data>> {
|
||||||
|
const basePaginatedResult: Partial<AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number]> = {
|
||||||
|
size: newPage,
|
||||||
|
isLoading: false,
|
||||||
|
isLoadingMore: false,
|
||||||
|
called: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response: FetchResult<Data> = {};
|
||||||
|
try {
|
||||||
|
const { signal, abortRequest } = this.createAbortController();
|
||||||
|
setAbortRequest(abortRequest);
|
||||||
|
|
||||||
|
setResult({
|
||||||
|
...getResultIdInfo(),
|
||||||
|
...createPaginatedResult({ isLoading: true, abortRequest, size: newPage, called: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newPage !== 1 && cachedPages.size) {
|
||||||
|
await revalidate(newPage, abortRequest, signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { response: request } = this.doRequestNew<Data, Variables>(
|
||||||
|
GQLMethod.MUTATION,
|
||||||
|
documentNode,
|
||||||
|
getVariablesFor(newPage),
|
||||||
|
{ ...options, context: { fetchOptions: { signal } } },
|
||||||
|
);
|
||||||
|
|
||||||
|
response = await request;
|
||||||
|
|
||||||
|
basePaginatedResult.data = response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof ApolloError) {
|
||||||
|
basePaginatedResult.error = error;
|
||||||
|
} else {
|
||||||
|
basePaginatedResult.error = new ApolloError({
|
||||||
|
errorMessage: error?.message ?? error.toString(),
|
||||||
|
extraInfo: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPaginatedResult = {
|
||||||
|
...getResultIdInfo(),
|
||||||
|
...createPaginatedResult(basePaginatedResult),
|
||||||
|
};
|
||||||
|
|
||||||
|
setResult(fetchPaginatedResult);
|
||||||
|
|
||||||
|
const shouldCacheResult = !fetchPaginatedResult.error;
|
||||||
|
if (shouldCacheResult) {
|
||||||
|
const currentCachedPages = this.cache.getResponseFor<Set<number>>(cachePagesKey, getVariablesFor(0)) ?? [];
|
||||||
|
this.cache.cacheResponse(cachePagesKey, getVariablesFor(0), new Set([...currentCachedPages, newPage]));
|
||||||
|
this.cache.cacheResponse(cacheResultsKey, getVariablesFor(newPage), fetchPaginatedResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchInitialPages<Data = any, Variables extends OperationVariables = OperationVariables>(
|
||||||
|
options: ApolloPaginatedMutationOptions<Data, Variables> | undefined,
|
||||||
|
areFetchingInitialPages: boolean,
|
||||||
|
areInitialPagesFetched: boolean,
|
||||||
|
setRevalidationDone: (isDone: boolean) => void,
|
||||||
|
cacheInitialPagesKey: string,
|
||||||
|
getVariablesFor: (page: number) => Variables,
|
||||||
|
initialPages: number,
|
||||||
|
fetchPage: (page: number) => Promise<FetchResult<Data>>,
|
||||||
|
hasNextPage: (result: FetchResult<Data>) => boolean,
|
||||||
|
): void {
|
||||||
|
const shouldFetchInitialPages = !options?.skipRequest && !areFetchingInitialPages && !areInitialPagesFetched;
|
||||||
|
if (shouldFetchInitialPages) {
|
||||||
|
setRevalidationDone(true);
|
||||||
|
this.cache.cacheResponse(cacheInitialPagesKey, getVariablesFor(0), true);
|
||||||
|
|
||||||
|
const loadInitialPages = async (initialPage: number) => {
|
||||||
|
const areAllPagesFetched = initialPage > initialPages;
|
||||||
|
if (areAllPagesFetched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageResult = await fetchPage(initialPage);
|
||||||
|
|
||||||
|
if (hasNextPage(pageResult)) {
|
||||||
|
await loadInitialPages(initialPage + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadInitialPages(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private returnPaginatedMutationResult<Data = any, Variables extends OperationVariables = OperationVariables>(
|
||||||
|
areInitialPagesFetched: boolean,
|
||||||
|
cachedResults: AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number][],
|
||||||
|
getVariablesFor: (page: number) => Variables,
|
||||||
|
paginatedResult: AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number],
|
||||||
|
fetchPage: (page: number) => Promise<FetchResult<Data>>,
|
||||||
|
hasCachedResult: boolean,
|
||||||
|
createPaginatedResult: (
|
||||||
|
result: Partial<AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number]>,
|
||||||
|
) => AbortableApolloUseMutationPaginatedResponse<Data, Variables>[1][number],
|
||||||
|
): AbortableApolloUseMutationPaginatedResponse<Data, Variables> {
|
||||||
|
const doCachedResultsExist = areInitialPagesFetched && cachedResults.length;
|
||||||
|
if (!doCachedResultsExist) {
|
||||||
|
return [fetchPage, [paginatedResult]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const areAllPagesCached = doCachedResultsExist && hasCachedResult;
|
||||||
|
if (!areAllPagesCached) {
|
||||||
|
return [fetchPage, [...cachedResults, paginatedResult]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
fetchPage,
|
||||||
|
[
|
||||||
|
...cachedResults.slice(0, cachedResults.length - 1),
|
||||||
|
createPaginatedResult({
|
||||||
|
...cachedResults[cachedResults.length - 1],
|
||||||
|
isValidating: paginatedResult.isValidating,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private revalidateInitialPages<Variables extends OperationVariables = OperationVariables>(
|
||||||
|
isRevalidationDone: boolean,
|
||||||
|
cachedResultsLength: number,
|
||||||
|
cachedPages: Set<number>,
|
||||||
|
setRevalidationDone: (isDone: boolean) => void,
|
||||||
|
getVariablesFor: (page: number) => Variables,
|
||||||
|
triggerRerender: () => void,
|
||||||
|
revalidate: (
|
||||||
|
maxPage: number,
|
||||||
|
abortRequest: AbortableRequest['abortRequest'],
|
||||||
|
signal: AbortSignal,
|
||||||
|
) => Promise<void>,
|
||||||
|
): void {
|
||||||
|
const isMountedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isRevalidationRequired = isMountedRef.current && cachedResultsLength;
|
||||||
|
if (!isRevalidationRequired) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRevalidationDone(false);
|
||||||
|
triggerRerender();
|
||||||
|
}, [JSON.stringify(getVariablesFor(0))]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldRevalidateData = isMountedRef.current && !isRevalidationDone && cachedResultsLength;
|
||||||
|
if (shouldRevalidateData) {
|
||||||
|
setRevalidationDone(true);
|
||||||
|
|
||||||
|
const { signal, abortRequest } = this.createAbortController();
|
||||||
|
revalidate(Math.max(...cachedPages), abortRequest, signal);
|
||||||
|
}
|
||||||
|
}, [isMountedRef.current, isRevalidationDone]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isMountedRef.current = true;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
public getValidImgUrlFor(imageUrl: string, apiVersion: string = ''): string {
|
public getValidImgUrlFor(imageUrl: string, apiVersion: string = ''): string {
|
||||||
const useCache = storage.getItem('useCache', true);
|
const useCache = storage.getItem('useCache', true);
|
||||||
const useCacheQuery = `?useCache=${useCache}`;
|
const useCacheQuery = `?useCache=${useCache}`;
|
||||||
@@ -339,13 +698,7 @@ export class RequestManager {
|
|||||||
| AbortableApolloUseQueryResponse<Data, Variables>
|
| AbortableApolloUseQueryResponse<Data, Variables>
|
||||||
| AbortableApolloUseMutationResponse<Data, Variables>
|
| AbortableApolloUseMutationResponse<Data, Variables>
|
||||||
| AbortableApolloMutationResponse<Data> {
|
| AbortableApolloMutationResponse<Data> {
|
||||||
const abortController = new AbortController();
|
const { signal, abortRequest } = this.createAbortController();
|
||||||
const abortRequest = (reason?: any): void => {
|
|
||||||
if (!abortController.signal.aborted) {
|
|
||||||
abortController.abort(reason);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case GQLMethod.USE_QUERY:
|
case GQLMethod.USE_QUERY:
|
||||||
return {
|
return {
|
||||||
@@ -356,7 +709,7 @@ export class RequestManager {
|
|||||||
context: {
|
context: {
|
||||||
...options?.context,
|
...options?.context,
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
signal: abortController.signal,
|
signal,
|
||||||
...options?.context?.fetchOptions,
|
...options?.context?.fetchOptions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -372,7 +725,7 @@ export class RequestManager {
|
|||||||
context: {
|
context: {
|
||||||
...options?.context,
|
...options?.context,
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
signal: abortController.signal,
|
signal,
|
||||||
...options?.context?.fetchOptions,
|
...options?.context?.fetchOptions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -388,7 +741,7 @@ export class RequestManager {
|
|||||||
context: {
|
context: {
|
||||||
...options?.context,
|
...options?.context,
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
signal: abortController.signal,
|
signal,
|
||||||
...options?.context?.fetchOptions,
|
...options?.context?.fetchOptions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -640,125 +993,209 @@ export class RequestManager {
|
|||||||
|
|
||||||
public useGetSourceMangas(
|
public useGetSourceMangas(
|
||||||
input: FetchSourceMangaInput,
|
input: FetchSourceMangaInput,
|
||||||
options?: MutationHookOptions<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>,
|
initialPages: number = 1,
|
||||||
|
options?: ApolloPaginatedMutationOptions<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>,
|
||||||
): AbortableApolloUseMutationPaginatedResponse<
|
): AbortableApolloUseMutationPaginatedResponse<
|
||||||
GetSourceMangasFetchMutation,
|
GetSourceMangasFetchMutation,
|
||||||
GetSourceMangasFetchMutationVariables
|
GetSourceMangasFetchMutationVariables
|
||||||
> {
|
> {
|
||||||
|
type MutationResult = AbortableApolloUseMutationPaginatedResponse<
|
||||||
|
GetSourceMangasFetchMutation,
|
||||||
|
GetSourceMangasFetchMutationVariables
|
||||||
|
>[1];
|
||||||
|
type MutationDataResult = MutationResult[number];
|
||||||
|
|
||||||
const createPaginatedResult = (
|
const createPaginatedResult = (
|
||||||
result: AbortableApolloUseMutationResponse[1],
|
result?: Partial<AbortableApolloUseMutationPaginatedResponse[1][number]> | null,
|
||||||
page: number,
|
page?: number,
|
||||||
): AbortableApolloUseMutationPaginatedResponse[1][number] => {
|
) => this.createPaginatedResult(result, input.page, page);
|
||||||
const loading = result.loading || !result.called;
|
|
||||||
return {
|
const getVariablesFor = (page: number): GetSourceMangasFetchMutationVariables => ({
|
||||||
...result,
|
input: {
|
||||||
loading,
|
...input,
|
||||||
size: page,
|
page,
|
||||||
loadingMore: loading && page > 1,
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
|
const CACHE_INITIAL_PAGES_FETCHING_KEY = 'GET_SOURCE_MANGAS_FETCH_FETCHING_INITIAL_PAGES';
|
||||||
|
const CACHE_PAGES_KEY = 'GET_SOURCE_MANGAS_FETCH_PAGES';
|
||||||
|
const CACHE_RESULTS_KEY = 'GET_SOURCE_MANGAS_FETCH';
|
||||||
|
|
||||||
|
const isRevalidationDoneRef = useRef(false);
|
||||||
|
const activeRevalidationRef = useRef<
|
||||||
|
| [
|
||||||
|
ForInput: GetSourceMangasFetchMutationVariables,
|
||||||
|
Request: Promise<unknown>,
|
||||||
|
AbortRequest: AbortableRequest['abortRequest'],
|
||||||
|
]
|
||||||
|
| null
|
||||||
|
>(null);
|
||||||
|
const abortRequestRef = useRef<AbortableRequest['abortRequest']>(() => {});
|
||||||
|
const resultRef = useRef<(MutationDataResult & { forInput: string }) | null>(null);
|
||||||
|
const result = resultRef.current;
|
||||||
|
|
||||||
|
const [, setTriggerRerender] = useState(0);
|
||||||
|
const triggerRerender = () => setTriggerRerender((prev) => prev + 1);
|
||||||
|
const setResult = (nextResult: typeof resultRef.current) => {
|
||||||
|
resultRef.current = nextResult;
|
||||||
|
triggerRerender();
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO - implement caching
|
const cachedPages = this.cache.getResponseFor<Set<number>>(CACHE_PAGES_KEY, getVariablesFor(0)) ?? new Set();
|
||||||
// - ? global cache with revalidating (same as SWR does, revalidate each page starting with 1st until the first page is reached whose data didn't change)
|
const cachedResults = [...cachedPages]
|
||||||
// - ? saving fetched mangas in location state and only "cache" when navigating prev/next
|
.map(
|
||||||
const [mutate, result] = this.doRequestNew<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>(
|
(cachedPage) =>
|
||||||
GQLMethod.USE_MUTATION,
|
this.cache.getResponseFor<MutationDataResult>(CACHE_RESULTS_KEY, getVariablesFor(cachedPage))!,
|
||||||
GET_SOURCE_MANGAS_FETCH,
|
)
|
||||||
{ input },
|
.sort((a, b) => a.size - b.size);
|
||||||
options,
|
const areFetchingInitialPages = !!this.cache.getResponseFor<boolean>(
|
||||||
|
CACHE_INITIAL_PAGES_FETCHING_KEY,
|
||||||
|
getVariablesFor(0),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [previousResults, setPreviousResults] = useState<AbortableApolloUseMutationPaginatedResponse[1]>([
|
const areInitialPagesFetched = cachedResults.length >= initialPages;
|
||||||
createPaginatedResult(result, input.page),
|
const isResultForCurrentInput = result?.forInput === JSON.stringify(getVariablesFor(0));
|
||||||
]);
|
const lastPage = cachedPages.size ? Math.max(...cachedPages) : input.page;
|
||||||
|
const nextPage = isResultForCurrentInput ? result.size : lastPage;
|
||||||
|
|
||||||
const [contentType, setContentType] = useState(input.type);
|
const paginatedResult =
|
||||||
const [query, setQuery] = useState(input.query);
|
isResultForCurrentInput && areInitialPagesFetched ? result : createPaginatedResult(undefined, nextPage);
|
||||||
const [page, setPage] = useState(input.page);
|
paginatedResult.abortRequest = abortRequestRef.current;
|
||||||
|
|
||||||
const paginatedResult = createPaginatedResult(result, page);
|
// make sure that the result is always for the current input
|
||||||
|
resultRef.current = { forInput: JSON.stringify(getVariablesFor(0)), ...paginatedResult };
|
||||||
|
|
||||||
// TODO - option "global cache with revalidating"
|
const hasCachedResult = !!this.cache.getResponseFor(CACHE_RESULTS_KEY, getVariablesFor(nextPage));
|
||||||
// replace previousResults with cache
|
|
||||||
// cache specific response
|
const revalidatePage = async (pageToRevalidate: number, maxPage: number, signal: AbortSignal) =>
|
||||||
// cache "base" key to specific page keys to be able to retrieve all necessary cached pages
|
this.revalidatePage(
|
||||||
// get cached results
|
CACHE_RESULTS_KEY,
|
||||||
// revalidate in background - revalidate first page -> result changed? revalidate every page until cached result and response is the same
|
CACHE_PAGES_KEY,
|
||||||
|
getVariablesFor,
|
||||||
|
options,
|
||||||
|
(cachedResult, revalidatedResult) =>
|
||||||
|
!cachedResult ||
|
||||||
|
!cachedResult.data?.fetchSourceManga.mangas.length ||
|
||||||
|
cachedResult.data.fetchSourceManga.mangas.some(
|
||||||
|
(manga, index) => manga.id !== revalidatedResult.data?.fetchSourceManga.mangas[index]?.id,
|
||||||
|
),
|
||||||
|
(revalidatedResult) => !!revalidatedResult.data?.fetchSourceManga.hasNextPage,
|
||||||
|
pageToRevalidate,
|
||||||
|
maxPage,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
const revalidate = async (
|
||||||
|
maxPage: number,
|
||||||
|
abortRequest: AbortableRequest['abortRequest'],
|
||||||
|
signal: AbortSignal,
|
||||||
|
) =>
|
||||||
|
this.revalidatePages(
|
||||||
|
activeRevalidationRef.current,
|
||||||
|
(isDone) => {
|
||||||
|
isRevalidationDoneRef.current = isDone;
|
||||||
|
},
|
||||||
|
(activeRevalidation) => {
|
||||||
|
activeRevalidationRef.current = activeRevalidation;
|
||||||
|
},
|
||||||
|
getVariablesFor,
|
||||||
|
(isValidating) => {
|
||||||
|
setResult({
|
||||||
|
...createPaginatedResult(resultRef.current),
|
||||||
|
isValidating,
|
||||||
|
forInput: JSON.stringify(getVariablesFor(0)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
revalidatePage,
|
||||||
|
maxPage,
|
||||||
|
abortRequest,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
|
||||||
// wrap "mutate" function to align with the expected type, which allows only passing a "page" argument
|
// wrap "mutate" function to align with the expected type, which allows only passing a "page" argument
|
||||||
const wrappedMutate = (newPage: number) => {
|
const wrappedMutate = async (newPage: number) =>
|
||||||
const resetPreviousResultForInitialLoad = newPage < page;
|
this.fetchPaginatedMutationPage<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>(
|
||||||
if (resetPreviousResultForInitialLoad) {
|
getVariablesFor,
|
||||||
setPreviousResults(previousResults.filter((prevResult) => prevResult.size <= newPage));
|
(abortRequest) => {
|
||||||
}
|
abortRequestRef.current = abortRequest;
|
||||||
|
|
||||||
if (newPage !== page) {
|
|
||||||
setPage(newPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mutate({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
...input,
|
|
||||||
page: newPage,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
() => ({ forType: input.type, forQuery: input.query }),
|
||||||
};
|
createPaginatedResult,
|
||||||
|
setResult,
|
||||||
|
revalidate,
|
||||||
|
options,
|
||||||
|
GET_SOURCE_MANGAS_FETCH,
|
||||||
|
CACHE_PAGES_KEY,
|
||||||
|
CACHE_RESULTS_KEY,
|
||||||
|
cachedPages,
|
||||||
|
newPage,
|
||||||
|
);
|
||||||
|
|
||||||
const contentTypeChanged = contentType !== input.type;
|
this.fetchInitialPages(
|
||||||
const queryChanged = query !== input.query;
|
options,
|
||||||
// instantly return empty results in case the provided variables changed - wait until the hook returns empty data,
|
areFetchingInitialPages,
|
||||||
// otherwise, updating the previous results will revert the reset
|
areInitialPagesFetched,
|
||||||
const resetPreviousResult = (queryChanged || contentTypeChanged) && !paginatedResult.data;
|
(isDone) => {
|
||||||
let updatedResults = [
|
isRevalidationDoneRef.current = isDone;
|
||||||
...(resetPreviousResult ? [{ ...paginatedResult, size: page, loadingMore: false }] : previousResults),
|
},
|
||||||
];
|
CACHE_INITIAL_PAGES_FETCHING_KEY,
|
||||||
|
getVariablesFor,
|
||||||
|
initialPages,
|
||||||
|
wrappedMutate,
|
||||||
|
(fetchedResult) => !!fetchedResult.data?.fetchSourceManga.hasNextPage,
|
||||||
|
);
|
||||||
|
|
||||||
if (resetPreviousResult) {
|
this.revalidateInitialPages(
|
||||||
setContentType(input.type);
|
isRevalidationDoneRef.current,
|
||||||
setQuery(input.query);
|
cachedResults.length,
|
||||||
setPreviousResults([paginatedResult]);
|
cachedPages,
|
||||||
}
|
(isDone) => {
|
||||||
|
isRevalidationDoneRef.current = isDone;
|
||||||
|
},
|
||||||
|
getVariablesFor,
|
||||||
|
triggerRerender,
|
||||||
|
revalidate,
|
||||||
|
);
|
||||||
|
|
||||||
const resultChanged = previousResults[page - 1]?.loading !== paginatedResult.loading;
|
return this.returnPaginatedMutationResult(
|
||||||
const updatePreviousResult = resultChanged && !resetPreviousResult;
|
areInitialPagesFetched,
|
||||||
if (updatePreviousResult) {
|
cachedResults,
|
||||||
updatedResults = [...previousResults.slice(0, page - 1), paginatedResult];
|
getVariablesFor,
|
||||||
setPreviousResults(updatedResults);
|
paginatedResult,
|
||||||
}
|
wrappedMutate,
|
||||||
|
hasCachedResult,
|
||||||
return [wrappedMutate, updatedResult];
|
createPaginatedResult,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public useGetSourcePopularMangas(
|
public useGetSourcePopularMangas(
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
initialPages?: number,
|
initialPages?: number,
|
||||||
swrOptions?: SWRInfiniteOptions<PaginatedMangaList>,
|
options?: ApolloPaginatedMutationOptions<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>,
|
||||||
): AbortableSWRInfiniteResponse<PaginatedMangaList> {
|
): AbortableApolloUseMutationPaginatedResponse<
|
||||||
return this.doRequest(SWRHttpMethod.SWR_GET_INFINITE, '', {
|
GetSourceMangasFetchMutation,
|
||||||
swrOptions: {
|
GetSourceMangasFetchMutationVariables
|
||||||
getEndpoint: (page, previousData) =>
|
> {
|
||||||
previousData?.hasNextPage ?? true ? `source/${sourceId}/popular/${page + 1}` : null,
|
return this.useGetSourceMangas(
|
||||||
initialSize: initialPages,
|
{ type: FetchSourceMangaType.Popular, source: sourceId, page: 1 },
|
||||||
...swrOptions,
|
initialPages,
|
||||||
} as typeof swrOptions,
|
options,
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public useGetSourceLatestMangas(
|
public useGetSourceLatestMangas(
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
initialPages?: number,
|
initialPages?: number,
|
||||||
swrOptions?: SWRInfiniteOptions<PaginatedMangaList>,
|
options?: ApolloPaginatedMutationOptions<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>,
|
||||||
): AbortableSWRInfiniteResponse<PaginatedMangaList> {
|
): AbortableApolloUseMutationPaginatedResponse<
|
||||||
return this.doRequest(SWRHttpMethod.SWR_GET_INFINITE, '', {
|
GetSourceMangasFetchMutation,
|
||||||
swrOptions: {
|
GetSourceMangasFetchMutationVariables
|
||||||
getEndpoint: (page, previousData) =>
|
> {
|
||||||
previousData?.hasNextPage ?? true ? `source/${sourceId}/latest/${page + 1}` : null,
|
return this.useGetSourceMangas(
|
||||||
initialSize: initialPages,
|
{ type: FetchSourceMangaType.Latest, source: sourceId, page: 1 },
|
||||||
...swrOptions,
|
initialPages,
|
||||||
} as typeof swrOptions,
|
options,
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public useGetSourcePreferences(
|
public useGetSourcePreferences(
|
||||||
@@ -790,14 +1227,19 @@ export class RequestManager {
|
|||||||
|
|
||||||
public useSourceSearch(
|
public useSourceSearch(
|
||||||
source: string,
|
source: string,
|
||||||
query: string,
|
query?: string,
|
||||||
filters?: FilterChangeInput[],
|
filters?: FilterChangeInput[],
|
||||||
options?: MutationHookOptions<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>,
|
initialPages?: number,
|
||||||
|
options?: ApolloPaginatedMutationOptions<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>,
|
||||||
): AbortableApolloUseMutationPaginatedResponse<
|
): AbortableApolloUseMutationPaginatedResponse<
|
||||||
GetSourceMangasFetchMutation,
|
GetSourceMangasFetchMutation,
|
||||||
GetSourceMangasFetchMutationVariables
|
GetSourceMangasFetchMutationVariables
|
||||||
> {
|
> {
|
||||||
return this.useGetSourceMangas({ type: FetchSourceMangaType.Search, source, query, filters, page: 1 }, options);
|
return this.useGetSourceMangas(
|
||||||
|
{ type: FetchSourceMangaType.Search, source, query, filters, page: 1 },
|
||||||
|
initialPages,
|
||||||
|
options,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public useSourceQuickSearch(
|
public useSourceQuickSearch(
|
||||||
|
|||||||
@@ -97,20 +97,15 @@ const SourceSearchPreview = React.memo(
|
|||||||
emptyQuery: boolean;
|
emptyQuery: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const skipRequest = !searchString;
|
|
||||||
|
|
||||||
const { id, displayName, lang } = source;
|
const { id, displayName, lang } = source;
|
||||||
const [loadPage, results] = requestManager.useSourceSearch(id, searchString ?? '', []);
|
const [, results] = requestManager.useSourceSearch(id, searchString ?? '', undefined, 1, {
|
||||||
const { data: searchResult, loading: isLoading, error, abortRequest } = results[0]!;
|
skipRequest: !searchString,
|
||||||
|
});
|
||||||
|
const { data: searchResult, isLoading, error, abortRequest } = results[0]!;
|
||||||
const mangas = (searchResult?.fetchSourceManga.mangas as MangaType[]) ?? [];
|
const mangas = (searchResult?.fetchSourceManga.mangas as MangaType[]) ?? [];
|
||||||
const noMangasFound = !isLoading && !mangas.length;
|
const noMangasFound = !isLoading && !mangas.length;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!skipRequest) {
|
|
||||||
loadPage(1);
|
|
||||||
}
|
|
||||||
}, [skipRequest, searchString]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onSearchRequestFinished(source, isLoading, !noMangasFound, !searchString);
|
onSearchRequestFinished(source, isLoading, !noMangasFound, !searchString);
|
||||||
}, [isLoading, noMangasFound, searchString]);
|
}, [isLoading, noMangasFound, searchString]);
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { Box, Button, styled, useTheme, useMediaQuery } from '@mui/material';
|
|||||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||||
import NewReleasesIcon from '@mui/icons-material/NewReleases';
|
import NewReleasesIcon from '@mui/icons-material/NewReleases';
|
||||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||||
import { IManga, PaginatedMangaList, TranslationKey } from '@/typings';
|
import { TranslationKey } from '@/typings';
|
||||||
import requestManager, { AbortableSWRInfiniteResponse } from '@/lib/requests/RequestManager.ts';
|
import requestManager, { AbortableApolloUseMutationPaginatedResponse } from '@/lib/requests/RequestManager.ts';
|
||||||
import { useDebounce } from '@/components/manga/hooks';
|
import { useDebounce } from '@/components/manga/hooks';
|
||||||
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||||
import SourceGridLayout from '@/components/source/GridLayouts';
|
import SourceGridLayout from '@/components/source/GridLayouts';
|
||||||
@@ -26,6 +26,11 @@ import AppbarSearch from '@/components/util/AppbarSearch';
|
|||||||
import SourceOptions from '@/components/source/SourceOptions';
|
import SourceOptions from '@/components/source/SourceOptions';
|
||||||
import NavbarContext from '@/components/context/NavbarContext';
|
import NavbarContext from '@/components/context/NavbarContext';
|
||||||
import SourceMangaGrid from '@/components/source/SourceMangaGrid';
|
import SourceMangaGrid from '@/components/source/SourceMangaGrid';
|
||||||
|
import {
|
||||||
|
GetSourceMangasFetchMutation,
|
||||||
|
GetSourceMangasFetchMutationVariables,
|
||||||
|
MangaType,
|
||||||
|
} from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
|
||||||
const ContentTypeMenu = styled('div')(({ theme }) => ({
|
const ContentTypeMenu = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -81,15 +86,8 @@ const SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY: { [contentType in SourceContentType]
|
|||||||
[SourceContentType.SEARCH]: 'manga.error.label.no_mangas_found',
|
[SourceContentType.SEARCH]: 'manga.error.label.no_mangas_found',
|
||||||
};
|
};
|
||||||
|
|
||||||
type SourceMangaResponse = Omit<AbortableSWRInfiniteResponse<PaginatedMangaList>, 'data'> & {
|
const getUniqueMangas = (mangas: MangaType[]): MangaType[] => {
|
||||||
data: {
|
const uniqueMangas: MangaType[] = [];
|
||||||
items: IManga[];
|
|
||||||
hasNextPage: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUniqueMangas = (mangas: IManga[]): IManga[] => {
|
|
||||||
const uniqueMangas: IManga[] = [];
|
|
||||||
|
|
||||||
mangas.forEach((manga) => {
|
mangas.forEach((manga) => {
|
||||||
const isDuplicate = uniqueMangas.some((uniqueManga) => uniqueManga.id === manga.id);
|
const isDuplicate = uniqueMangas.some((uniqueManga) => uniqueManga.id === manga.id);
|
||||||
@@ -106,9 +104,18 @@ const useSourceManga = (
|
|||||||
contentType: SourceContentType,
|
contentType: SourceContentType,
|
||||||
searchTerm: string | null | undefined,
|
searchTerm: string | null | undefined,
|
||||||
filters: IPos[],
|
filters: IPos[],
|
||||||
initialPages = 1,
|
initialPages: number,
|
||||||
): SourceMangaResponse => {
|
): [
|
||||||
let result: AbortableSWRInfiniteResponse<PaginatedMangaList>;
|
AbortableApolloUseMutationPaginatedResponse<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>[0],
|
||||||
|
AbortableApolloUseMutationPaginatedResponse<
|
||||||
|
GetSourceMangasFetchMutation,
|
||||||
|
GetSourceMangasFetchMutationVariables
|
||||||
|
>[1][number],
|
||||||
|
] => {
|
||||||
|
let result: AbortableApolloUseMutationPaginatedResponse<
|
||||||
|
GetSourceMangasFetchMutation,
|
||||||
|
GetSourceMangasFetchMutationVariables
|
||||||
|
>;
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case SourceContentType.POPULAR:
|
case SourceContentType.POPULAR:
|
||||||
result = requestManager.useGetSourcePopularMangas(sourceId, initialPages);
|
result = requestManager.useGetSourcePopularMangas(sourceId, initialPages);
|
||||||
@@ -117,45 +124,71 @@ const useSourceManga = (
|
|||||||
result = requestManager.useGetSourceLatestMangas(sourceId, initialPages);
|
result = requestManager.useGetSourceLatestMangas(sourceId, initialPages);
|
||||||
break;
|
break;
|
||||||
case SourceContentType.SEARCH:
|
case SourceContentType.SEARCH:
|
||||||
result = requestManager.useSourceQuickSearch(sourceId, searchTerm ?? '', [], initialPages);
|
result = requestManager.useSourceSearch(sourceId, searchTerm ?? '', undefined, initialPages);
|
||||||
break;
|
break;
|
||||||
case SourceContentType.FILTER:
|
case SourceContentType.FILTER:
|
||||||
result = requestManager.useSourceQuickSearch(
|
result = requestManager.useSourceSearch(sourceId, undefined, [], initialPages);
|
||||||
sourceId,
|
// TODO - update filters to gql
|
||||||
'',
|
// result = requestManager.useSourceQuickSearch(
|
||||||
filters.map((filter) => {
|
// sourceId,
|
||||||
const { position, state, group } = filter;
|
// '',
|
||||||
|
// filters.map((filter) => {
|
||||||
const isPartOfGroup = group !== undefined;
|
// const { position, state, group } = filter;
|
||||||
if (isPartOfGroup) {
|
//
|
||||||
return {
|
// const isPartOfGroup = group !== undefined;
|
||||||
position: group,
|
// if (isPartOfGroup) {
|
||||||
state: JSON.stringify({
|
// return {
|
||||||
position,
|
// position: group,
|
||||||
state,
|
// state: JSON.stringify({
|
||||||
}),
|
// position,
|
||||||
};
|
// state,
|
||||||
}
|
// }),
|
||||||
|
// };
|
||||||
return filter;
|
// }
|
||||||
}),
|
//
|
||||||
initialPages,
|
// return filter;
|
||||||
{ disableCache: true },
|
// }),
|
||||||
);
|
// initialPages,
|
||||||
|
// { disableCache: true },
|
||||||
|
// );
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown ContentType "${contentType}"`);
|
throw new Error(`Unknown ContentType "${contentType}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pages = result.data;
|
const pages = result[1]!;
|
||||||
const { hasNextPage } = pages?.[pages.length - 1] ?? { hasNextPage: false };
|
const lastLoadedPageIndex = pages.findLastIndex((page) => !!page.data?.fetchSourceManga);
|
||||||
|
const lastLoadedPage = pages[lastLoadedPageIndex];
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() => (pages ?? []).map((page) => page.mangaList).reduce((prevList, list) => [...prevList, ...list], []),
|
() =>
|
||||||
|
(pages ?? [])
|
||||||
|
.map((page) => page.data?.fetchSourceManga.mangas ?? [])
|
||||||
|
.reduce((prevList, list) => [...prevList, ...list], []),
|
||||||
[pages],
|
[pages],
|
||||||
);
|
) as MangaType[];
|
||||||
const uniqueItems = useMemo(() => getUniqueMangas(items), [items]);
|
const uniqueItems = useMemo(() => getUniqueMangas(items), [items]);
|
||||||
|
|
||||||
return { ...result, data: { items: uniqueItems, hasNextPage } };
|
if (!uniqueItems.length) {
|
||||||
|
return [result[0] as any, result[1][result[1].length - 1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
result[0],
|
||||||
|
{
|
||||||
|
...pages[pages.length - 1],
|
||||||
|
data: {
|
||||||
|
...lastLoadedPage!.data,
|
||||||
|
fetchSourceManga: {
|
||||||
|
...lastLoadedPage!.data!.fetchSourceManga,
|
||||||
|
hasNextPage:
|
||||||
|
pages.length > lastLoadedPageIndex + 1
|
||||||
|
? false
|
||||||
|
: lastLoadedPage!.data!.fetchSourceManga.hasNextPage,
|
||||||
|
mangas: uniqueItems,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SourceMangas() {
|
export default function SourceMangas() {
|
||||||
@@ -179,17 +212,19 @@ export default function SourceMangas() {
|
|||||||
const searchTerm = useDebounce(query, 1000);
|
const searchTerm = useDebounce(query, 1000);
|
||||||
const [resetScrollPosition, setResetScrollPosition] = useState(false);
|
const [resetScrollPosition, setResetScrollPosition] = useState(false);
|
||||||
const [contentType, setContentType] = useState(currentLocationContentType);
|
const [contentType, setContentType] = useState(currentLocationContentType);
|
||||||
const {
|
const [loadPage, { data, isLoading, size: lastPageNum, abortRequest }] = useSourceManga(
|
||||||
data: { items: mangas, hasNextPage } = { items: [], hasNextPage: false },
|
sourceId,
|
||||||
isLoading,
|
contentType,
|
||||||
size: lastPageNum,
|
searchTerm,
|
||||||
setSize: setPages,
|
filtersToApply,
|
||||||
mutate: refreshData,
|
isLargeScreen ? 2 : 1,
|
||||||
abortRequest,
|
);
|
||||||
} = useSourceManga(sourceId, contentType, searchTerm, filtersToApply, isLargeScreen ? 2 : 1);
|
const mangas = (data?.fetchSourceManga.mangas as MangaType[]) ?? [];
|
||||||
|
const hasNextPage = data?.fetchSourceManga.hasNextPage ?? false;
|
||||||
|
|
||||||
const { data: filters = [], mutate: mutateFilters } = requestManager.useGetSourceFilters(sourceId);
|
const { data: filters = [], mutate: mutateFilters } = requestManager.useGetSourceFilters(sourceId);
|
||||||
const { data } = requestManager.useGetSource(sourceId);
|
const { data: sourceData } = requestManager.useGetSource(sourceId);
|
||||||
const source = data?.source;
|
const source = sourceData?.source;
|
||||||
const [triggerDataRefresh, setTriggerDataRefresh] = useState(false);
|
const [triggerDataRefresh, setTriggerDataRefresh] = useState(false);
|
||||||
|
|
||||||
const message = !isLoading ? t(SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY[contentType]) : undefined;
|
const message = !isLoading ? t(SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY[contentType]) : undefined;
|
||||||
@@ -231,8 +266,8 @@ export default function SourceMangas() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPages(lastPageNum + 1);
|
loadPage(lastPageNum + 1);
|
||||||
}, [setPages, lastPageNum, hasNextPage]);
|
}, [lastPageNum, hasNextPage, contentType]);
|
||||||
|
|
||||||
const resetFilters = useCallback(async () => {
|
const resetFilters = useCallback(async () => {
|
||||||
setDialogFiltersToApply([]);
|
setDialogFiltersToApply([]);
|
||||||
@@ -262,12 +297,13 @@ export default function SourceMangas() {
|
|||||||
[searchTerm, contentType],
|
[searchTerm, contentType],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO - check when fixing filters
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!triggerDataRefresh) {
|
if (!triggerDataRefresh) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshData();
|
// refreshData();
|
||||||
setTriggerDataRefresh(false);
|
setTriggerDataRefresh(false);
|
||||||
}, [triggerDataRefresh]);
|
}, [triggerDataRefresh]);
|
||||||
|
|
||||||
@@ -327,6 +363,7 @@ export default function SourceMangas() {
|
|||||||
</ContentTypeButton>
|
</ContentTypeButton>
|
||||||
</ContentTypeMenu>
|
</ContentTypeMenu>
|
||||||
<SourceMangaGrid
|
<SourceMangaGrid
|
||||||
|
key={contentType}
|
||||||
mangas={mangas}
|
mangas={mangas}
|
||||||
hasNextPage={hasNextPage}
|
hasNextPage={hasNextPage}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
|
|||||||
Reference in New Issue
Block a user