diff --git a/src/components/ExtensionCard.tsx b/src/components/ExtensionCard.tsx index bfbf5f7e..2825f30b 100644 --- a/src/components/ExtensionCard.tsx +++ b/src/components/ExtensionCard.tsx @@ -14,12 +14,11 @@ import Avatar from '@mui/material/Avatar'; import Typography from '@mui/material/Typography'; import { Box } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { IExtension, TranslationKey } from '@/typings'; +import { Extension, TranslationKey } from '@/typings'; import requestManager from '@/lib/requests/RequestManager.ts'; interface IProps { - extension: IExtension; - notifyInstall: () => void; + extension: Extension; } enum ExtensionAction { @@ -65,17 +64,16 @@ export default function ExtensionCard(props: IProps) { const { t } = useTranslation(); const { - extension: { name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl, isNsfw }, - notifyInstall, + extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw }, } = props; const [installedState, setInstalledState] = useState(() => { - if (obsolete) { + if (isObsolete) { return InstalledState.OBSOLETE; } if (hasUpdate) { return InstalledState.UPDATE; } - return installed ? InstalledState.UNINSTALL : InstalledState.INSTALL; + return isInstalled ? InstalledState.UNINSTALL : InstalledState.INSTALL; }); const langPress = lang === 'all' ? t('extension.language.all') : lang.toUpperCase(); @@ -87,19 +85,18 @@ export default function ExtensionCard(props: IProps) { setInstalledState(state); switch (action) { case ExtensionAction.INSTALL: - await requestManager.installExtension(pkgName).response; + await requestManager.updateExtension(pkgName, { install: true }).response; break; case ExtensionAction.UNINSTALL: - await requestManager.uninstallExtension(pkgName).response; + await requestManager.updateExtension(pkgName, { uninstall: true }).response; break; case ExtensionAction.UPDATE: - await requestManager.updateExtension(pkgName).response; + await requestManager.updateExtension(pkgName, { update: true }).response; break; default: throw new Error(`Unexpected ExtensionAction "${action}"`); } setInstalledState(nextAction); - notifyInstall(); }; function handleButtonClick() { diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 57c97580..84df632c 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -26,7 +26,6 @@ import { BatchChaptersChange, ICategory, IChapter, - IExtension, IManga, IMangaChapter, IncludeInGlobalUpdate, @@ -46,14 +45,29 @@ import { CheckForServerUpdatesQueryVariables, GetAboutQuery, GetAboutQueryVariables, + GetExtensionsFetchMutation, + GetExtensionsFetchMutationVariables, + GetExtensionsQuery, + GetExtensionsQueryVariables, GetGlobalMetadatasQuery, GetGlobalMetadatasQueryVariables, + InstallExternalExtensionMutation, + InstallExternalExtensionMutationVariables, SetGlobalMetadataMutation, SetGlobalMetadataMutationVariables, + UpdateExtensionMutation, + UpdateExtensionMutationVariables, + UpdateExtensionPatchInput, } from '@/lib/graphql/generated/graphql.ts'; import { GET_GLOBAL_METADATA, GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts'; import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts'; import { CHECK_FOR_SERVER_UPDATES, GET_ABOUT } from '@/lib/graphql/queries/ServerInfoQuery.ts'; +import { GET_EXTENSION, GET_EXTENSIONS } from '@/lib/graphql/queries/ExtensionQuery.ts'; +import { + GET_EXTENSIONS_FETCH, + INSTALL_EXTERNAL_EXTENSION, + UPDATE_EXTENSION, +} from '@/lib/graphql/mutations/ExtensionMutation.ts'; enum SWRHttpMethod { SWR_GET, @@ -460,24 +474,39 @@ export class RequestManager { return this.doRequestNew(GQLMethod.USE_QUERY, CHECK_FOR_SERVER_UPDATES, {}, options); } - public useGetExtensionList(swrOptions?: SWROptions): AbortableSWRResponse { - return this.doRequest(HttpMethod.SWR_GET, 'extension/list', { swrOptions }); + public useGetExtensionList( + options?: QueryHookOptions, + ): AbortableApolloUseQueryResponse { + return this.doRequestNew(GQLMethod.USE_QUERY, GET_EXTENSIONS, {}, options); } - public installExtension(extension: string | File): AbortableAxiosResponse { - if (typeof extension === 'string') { - return this.doRequest(HttpMethod.GET, `extension/install/${extension}`); - } - - return this.doRequest(HttpMethod.POST, `extension/install`, { formData: { file: extension } }); + public useExtensionListFetch( + options?: MutationHookOptions, + ): AbortableApolloUseMutationResponse { + return this.doRequestNew(GQLMethod.USE_MUTATION, GET_EXTENSIONS_FETCH, {}, options); } - public updateExtension(extension: string): AbortableAxiosResponse { - return this.doRequest(HttpMethod.GET, `extension/update/${extension}`); + public installExternalExtension( + extensionFile: File, + ): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + INSTALL_EXTERNAL_EXTENSION, + { file: extensionFile }, + { refetchQueries: [GET_EXTENSION, GET_EXTENSIONS] }, + ); } - public uninstallExtension(extension: string): AbortableAxiosResponse { - return this.doRequest(HttpMethod.GET, `extension/uninstall/${extension}`); + public updateExtension( + id: string, + patch: UpdateExtensionPatchInput, + ): AbortableApolloMutationResponse { + return this.doRequestNew( + GQLMethod.MUTATION, + UPDATE_EXTENSION, + { input: { id, patch } }, + { refetchQueries: [GET_EXTENSION, GET_EXTENSIONS, GET_SOURCES] }, + ); } public getExtensionIconUrl(extension: string): string { diff --git a/src/screens/Extensions.tsx b/src/screens/Extensions.tsx index e0dddc81..1cd323d4 100644 --- a/src/screens/Extensions.tsx +++ b/src/screens/Extensions.tsx @@ -14,7 +14,6 @@ import { StringParam, useQueryParam } from 'use-query-params'; import { Virtuoso } from 'react-virtuoso'; import { Typography, useMediaQuery, useTheme } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { IExtension } from '@/typings'; import requestManager from '@/lib/requests/RequestManager.ts'; import { extensionDefaultLangs, DefaultLanguage, langSortCmp } from '@/util/language'; import useLocalStorage from '@/util/useLocalStorage'; @@ -31,11 +30,12 @@ import { makeToaster } from '@/components/util/Toast'; import LangSelect from '@/components/navbar/action/LangSelect'; import NavbarContext from '@/components/context/NavbarContext'; import ExtensionCard from '@/components/ExtensionCard'; +import { Extension } from '@/typings.ts'; const LANGUAGE = 0; const EXTENSIONS = 1; -function getExtensionsInfo(extensions: IExtension[]): { +function getExtensionsInfo(extensions: Extension[]): { allLangs: string[]; groupedExtensions: GroupedExtensionsResult; } { @@ -55,12 +55,12 @@ function getExtensionsInfo(extensions: IExtension[]): { allLangs.push(extension.lang); } } - if (extension.installed) { + if (extension.isInstalled) { if (extension.hasUpdate) { sortedExtensions[ExtensionState.UPDATE_PENDING].push(extension); return; } - if (extension.obsolete) { + if (extension.isObsolete) { sortedExtensions[ExtensionState.OBSOLETE].push(extension); return; } @@ -100,7 +100,17 @@ export default function MangaExtensions() { const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const [query] = useQueryParam('query', StringParam); - const { data: allExtensions, mutate, isLoading } = requestManager.useGetExtensionList(); + const [extensionsTimestamp, setExtensionsTimestamp] = useLocalStorage('extensionsTimestamp', 0); + const [fetchExtensions, { loading: isFetching }] = requestManager.useExtensionListFetch(); + const { data, loading: isLoading } = requestManager.useGetExtensionList(); + const allExtensions = data?.extensions.nodes; + + useEffect(() => { + const updateExtensionsList = Date.now() - extensionsTimestamp >= 1000 * 60; // update list in case it's older than 1 minute + if (updateExtensionsList) { + fetchExtensions().catch(() => setExtensionsTimestamp(Date.now())); + } + }, []); const filteredExtensions = useMemo( () => @@ -122,7 +132,7 @@ export default function MangaExtensions() { [shownLangs, groupedExtensions], ); - const flatRenderItems: (IExtension | string)[] = filteredGroupedExtensions.flat(2); + const flatRenderItems: (Extension | string)[] = filteredGroupedExtensions.flat(2); const [toasts, makeToast] = makeToaster(useState([])); @@ -134,10 +144,9 @@ export default function MangaExtensions() { makeToast(t('extension.label.installing_file'), 'info'); requestManager - .installExtension(file) + .installExternalExtension(file) .response.then(() => { makeToast(t('extension.label.installed_successfully'), 'success'); - mutate(); }) .catch(() => makeToast(t('extension.label.installation_failed'), 'error')); } else { @@ -178,7 +187,7 @@ export default function MangaExtensions() { }; }, []); - if (isLoading) { + if (isLoading || isFetching) { return ; } @@ -220,17 +229,9 @@ export default function MangaExtensions() { ); } - const item = flatRenderItems[index] as IExtension; + const item = flatRenderItems[index] as Extension; - return ( - { - mutate(); - }} - /> - ); + return ; }} /> diff --git a/src/screens/util/Extensions.ts b/src/screens/util/Extensions.ts index 2ed2dcee..09e5d4de 100644 --- a/src/screens/util/Extensions.ts +++ b/src/screens/util/Extensions.ts @@ -7,7 +7,7 @@ */ import { t } from 'i18next'; -import { IExtension, TranslationKey } from '@/typings'; +import { Extension, TranslationKey } from '@/typings'; import { DefaultLanguage, langCodeToName } from '@/util/language'; export enum ExtensionState { @@ -16,16 +16,16 @@ export enum ExtensionState { OBSOLETE = 'OBSOLETE', } -export type GroupedExtensionsResult = [KEY, IExtension[]][]; +export type GroupedExtensionsResult = [KEY, Extension[]][]; export type GroupedByExtensionState = { - [state in ExtensionState]: IExtension[]; + [state in ExtensionState]: Extension[]; }; export type GroupedByLanguage = { - [language in DefaultLanguage]: IExtension[]; + [language in DefaultLanguage]: Extension[]; } & { - [language: string]: IExtension[]; + [language: string]: Extension[]; }; export type GroupedExtensions = GroupedByExtensionState & GroupedByLanguage; diff --git a/src/typings.ts b/src/typings.ts index d3c227f6..f3054f94 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -10,6 +10,7 @@ import { OverridableComponent } from '@mui/material/OverridableComponent'; import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon'; import { ParseKeys } from 'i18next'; import { Location } from 'react-router-dom'; +import { ExtensionType } from '@/lib/graphql/generated/graphql.ts'; type GenericLocation = Omit & { state?: State }; @@ -21,19 +22,7 @@ declare module 'react-router-dom' { export type TranslationKey = ParseKeys; -export interface IExtension { - name: string; - pkgName: string; - versionName: string; - versionCode: number; - lang: string; - isNsfw: boolean; - apkName: string; - iconUrl: string; - installed: boolean; - hasUpdate: boolean; - obsolete: boolean; -} +export type Extension = Omit; export interface ISource { id: string;