Use gql for "extensions"

This commit is contained in:
schroda
2023-09-01 20:28:23 +02:00
parent 81cfeedbba
commit 4a80e18cd0
5 changed files with 77 additions and 61 deletions

View File

@@ -14,12 +14,11 @@ import Avatar from '@mui/material/Avatar';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IExtension, TranslationKey } from '@/typings'; import { Extension, TranslationKey } from '@/typings';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
interface IProps { interface IProps {
extension: IExtension; extension: Extension;
notifyInstall: () => void;
} }
enum ExtensionAction { enum ExtensionAction {
@@ -65,17 +64,16 @@ export default function ExtensionCard(props: IProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
extension: { name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl, isNsfw }, extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw },
notifyInstall,
} = props; } = props;
const [installedState, setInstalledState] = useState<InstalledStates>(() => { const [installedState, setInstalledState] = useState<InstalledStates>(() => {
if (obsolete) { if (isObsolete) {
return InstalledState.OBSOLETE; return InstalledState.OBSOLETE;
} }
if (hasUpdate) { if (hasUpdate) {
return InstalledState.UPDATE; 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(); const langPress = lang === 'all' ? t('extension.language.all') : lang.toUpperCase();
@@ -87,19 +85,18 @@ export default function ExtensionCard(props: IProps) {
setInstalledState(state); setInstalledState(state);
switch (action) { switch (action) {
case ExtensionAction.INSTALL: case ExtensionAction.INSTALL:
await requestManager.installExtension(pkgName).response; await requestManager.updateExtension(pkgName, { install: true }).response;
break; break;
case ExtensionAction.UNINSTALL: case ExtensionAction.UNINSTALL:
await requestManager.uninstallExtension(pkgName).response; await requestManager.updateExtension(pkgName, { uninstall: true }).response;
break; break;
case ExtensionAction.UPDATE: case ExtensionAction.UPDATE:
await requestManager.updateExtension(pkgName).response; await requestManager.updateExtension(pkgName, { update: true }).response;
break; break;
default: default:
throw new Error(`Unexpected ExtensionAction "${action}"`); throw new Error(`Unexpected ExtensionAction "${action}"`);
} }
setInstalledState(nextAction); setInstalledState(nextAction);
notifyInstall();
}; };
function handleButtonClick() { function handleButtonClick() {

View File

@@ -26,7 +26,6 @@ import {
BatchChaptersChange, BatchChaptersChange,
ICategory, ICategory,
IChapter, IChapter,
IExtension,
IManga, IManga,
IMangaChapter, IMangaChapter,
IncludeInGlobalUpdate, IncludeInGlobalUpdate,
@@ -46,14 +45,29 @@ import {
CheckForServerUpdatesQueryVariables, CheckForServerUpdatesQueryVariables,
GetAboutQuery, GetAboutQuery,
GetAboutQueryVariables, GetAboutQueryVariables,
GetExtensionsFetchMutation,
GetExtensionsFetchMutationVariables,
GetExtensionsQuery,
GetExtensionsQueryVariables,
GetGlobalMetadatasQuery, GetGlobalMetadatasQuery,
GetGlobalMetadatasQueryVariables, GetGlobalMetadatasQueryVariables,
InstallExternalExtensionMutation,
InstallExternalExtensionMutationVariables,
SetGlobalMetadataMutation, SetGlobalMetadataMutation,
SetGlobalMetadataMutationVariables, SetGlobalMetadataMutationVariables,
UpdateExtensionMutation,
UpdateExtensionMutationVariables,
UpdateExtensionPatchInput,
} from '@/lib/graphql/generated/graphql.ts'; } from '@/lib/graphql/generated/graphql.ts';
import { GET_GLOBAL_METADATA, GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.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 { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts';
import { CHECK_FOR_SERVER_UPDATES, GET_ABOUT } from '@/lib/graphql/queries/ServerInfoQuery.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 { enum SWRHttpMethod {
SWR_GET, SWR_GET,
@@ -460,24 +474,39 @@ export class RequestManager {
return this.doRequestNew(GQLMethod.USE_QUERY, CHECK_FOR_SERVER_UPDATES, {}, options); return this.doRequestNew(GQLMethod.USE_QUERY, CHECK_FOR_SERVER_UPDATES, {}, options);
} }
public useGetExtensionList(swrOptions?: SWROptions<IExtension[]>): AbortableSWRResponse<IExtension[]> { public useGetExtensionList(
return this.doRequest(HttpMethod.SWR_GET, 'extension/list', { swrOptions }); options?: QueryHookOptions<GetExtensionsQuery, GetExtensionsQueryVariables>,
): AbortableApolloUseQueryResponse<GetExtensionsQuery, GetExtensionsQueryVariables> {
return this.doRequestNew(GQLMethod.USE_QUERY, GET_EXTENSIONS, {}, options);
} }
public installExtension(extension: string | File): AbortableAxiosResponse { public useExtensionListFetch(
if (typeof extension === 'string') { options?: MutationHookOptions<GetExtensionsFetchMutation, GetExtensionsFetchMutationVariables>,
return this.doRequest(HttpMethod.GET, `extension/install/${extension}`); ): AbortableApolloUseMutationResponse<GetExtensionsFetchMutation, GetExtensionsFetchMutationVariables> {
} return this.doRequestNew(GQLMethod.USE_MUTATION, GET_EXTENSIONS_FETCH, {}, options);
return this.doRequest(HttpMethod.POST, `extension/install`, { formData: { file: extension } });
} }
public updateExtension(extension: string): AbortableAxiosResponse { public installExternalExtension(
return this.doRequest(HttpMethod.GET, `extension/update/${extension}`); extensionFile: File,
): AbortableApolloMutationResponse<InstallExternalExtensionMutation> {
return this.doRequestNew<InstallExternalExtensionMutation, InstallExternalExtensionMutationVariables>(
GQLMethod.MUTATION,
INSTALL_EXTERNAL_EXTENSION,
{ file: extensionFile },
{ refetchQueries: [GET_EXTENSION, GET_EXTENSIONS] },
);
} }
public uninstallExtension(extension: string): AbortableAxiosResponse { public updateExtension(
return this.doRequest(HttpMethod.GET, `extension/uninstall/${extension}`); id: string,
patch: UpdateExtensionPatchInput,
): AbortableApolloMutationResponse<UpdateExtensionMutation> {
return this.doRequestNew<UpdateExtensionMutation, UpdateExtensionMutationVariables>(
GQLMethod.MUTATION,
UPDATE_EXTENSION,
{ input: { id, patch } },
{ refetchQueries: [GET_EXTENSION, GET_EXTENSIONS, GET_SOURCES] },
);
} }
public getExtensionIconUrl(extension: string): string { public getExtensionIconUrl(extension: string): string {

View File

@@ -14,7 +14,6 @@ import { StringParam, useQueryParam } from 'use-query-params';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
import { Typography, useMediaQuery, useTheme } from '@mui/material'; import { Typography, useMediaQuery, useTheme } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IExtension } from '@/typings';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import { extensionDefaultLangs, DefaultLanguage, langSortCmp } from '@/util/language'; import { extensionDefaultLangs, DefaultLanguage, langSortCmp } from '@/util/language';
import useLocalStorage from '@/util/useLocalStorage'; import useLocalStorage from '@/util/useLocalStorage';
@@ -31,11 +30,12 @@ import { makeToaster } from '@/components/util/Toast';
import LangSelect from '@/components/navbar/action/LangSelect'; import LangSelect from '@/components/navbar/action/LangSelect';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext from '@/components/context/NavbarContext';
import ExtensionCard from '@/components/ExtensionCard'; import ExtensionCard from '@/components/ExtensionCard';
import { Extension } from '@/typings.ts';
const LANGUAGE = 0; const LANGUAGE = 0;
const EXTENSIONS = 1; const EXTENSIONS = 1;
function getExtensionsInfo(extensions: IExtension[]): { function getExtensionsInfo(extensions: Extension[]): {
allLangs: string[]; allLangs: string[];
groupedExtensions: GroupedExtensionsResult; groupedExtensions: GroupedExtensionsResult;
} { } {
@@ -55,12 +55,12 @@ function getExtensionsInfo(extensions: IExtension[]): {
allLangs.push(extension.lang); allLangs.push(extension.lang);
} }
} }
if (extension.installed) { if (extension.isInstalled) {
if (extension.hasUpdate) { if (extension.hasUpdate) {
sortedExtensions[ExtensionState.UPDATE_PENDING].push(extension); sortedExtensions[ExtensionState.UPDATE_PENDING].push(extension);
return; return;
} }
if (extension.obsolete) { if (extension.isObsolete) {
sortedExtensions[ExtensionState.OBSOLETE].push(extension); sortedExtensions[ExtensionState.OBSOLETE].push(extension);
return; return;
} }
@@ -100,7 +100,17 @@ export default function MangaExtensions() {
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [query] = useQueryParam('query', StringParam); 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( const filteredExtensions = useMemo(
() => () =>
@@ -122,7 +132,7 @@ export default function MangaExtensions() {
[shownLangs, groupedExtensions], [shownLangs, groupedExtensions],
); );
const flatRenderItems: (IExtension | string)[] = filteredGroupedExtensions.flat(2); const flatRenderItems: (Extension | string)[] = filteredGroupedExtensions.flat(2);
const [toasts, makeToast] = makeToaster(useState<React.ReactElement[]>([])); const [toasts, makeToast] = makeToaster(useState<React.ReactElement[]>([]));
@@ -134,10 +144,9 @@ export default function MangaExtensions() {
makeToast(t('extension.label.installing_file'), 'info'); makeToast(t('extension.label.installing_file'), 'info');
requestManager requestManager
.installExtension(file) .installExternalExtension(file)
.response.then(() => { .response.then(() => {
makeToast(t('extension.label.installed_successfully'), 'success'); makeToast(t('extension.label.installed_successfully'), 'success');
mutate();
}) })
.catch(() => makeToast(t('extension.label.installation_failed'), 'error')); .catch(() => makeToast(t('extension.label.installation_failed'), 'error'));
} else { } else {
@@ -178,7 +187,7 @@ export default function MangaExtensions() {
}; };
}, []); }, []);
if (isLoading) { if (isLoading || isFetching) {
return <LoadingPlaceholder />; return <LoadingPlaceholder />;
} }
@@ -220,17 +229,9 @@ export default function MangaExtensions() {
</Typography> </Typography>
); );
} }
const item = flatRenderItems[index] as IExtension; const item = flatRenderItems[index] as Extension;
return ( return <ExtensionCard key={item.apkName} extension={item} />;
<ExtensionCard
key={item.apkName}
extension={item}
notifyInstall={() => {
mutate();
}}
/>
);
}} }}
/> />
</> </>

View File

@@ -7,7 +7,7 @@
*/ */
import { t } from 'i18next'; import { t } from 'i18next';
import { IExtension, TranslationKey } from '@/typings'; import { Extension, TranslationKey } from '@/typings';
import { DefaultLanguage, langCodeToName } from '@/util/language'; import { DefaultLanguage, langCodeToName } from '@/util/language';
export enum ExtensionState { export enum ExtensionState {
@@ -16,16 +16,16 @@ export enum ExtensionState {
OBSOLETE = 'OBSOLETE', OBSOLETE = 'OBSOLETE',
} }
export type GroupedExtensionsResult<KEY extends string = string> = [KEY, IExtension[]][]; export type GroupedExtensionsResult<KEY extends string = string> = [KEY, Extension[]][];
export type GroupedByExtensionState = { export type GroupedByExtensionState = {
[state in ExtensionState]: IExtension[]; [state in ExtensionState]: Extension[];
}; };
export type GroupedByLanguage = { export type GroupedByLanguage = {
[language in DefaultLanguage]: IExtension[]; [language in DefaultLanguage]: Extension[];
} & { } & {
[language: string]: IExtension[]; [language: string]: Extension[];
}; };
export type GroupedExtensions = GroupedByExtensionState & GroupedByLanguage; export type GroupedExtensions = GroupedByExtensionState & GroupedByLanguage;

View File

@@ -10,6 +10,7 @@ import { OverridableComponent } from '@mui/material/OverridableComponent';
import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon'; import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
import { ParseKeys } from 'i18next'; import { ParseKeys } from 'i18next';
import { Location } from 'react-router-dom'; import { Location } from 'react-router-dom';
import { ExtensionType } from '@/lib/graphql/generated/graphql.ts';
type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State }; type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State };
@@ -21,19 +22,7 @@ declare module 'react-router-dom' {
export type TranslationKey = ParseKeys; export type TranslationKey = ParseKeys;
export interface IExtension { export type Extension = Omit<ExtensionType, 'source'>;
name: string;
pkgName: string;
versionName: string;
versionCode: number;
lang: string;
isNsfw: boolean;
apkName: string;
iconUrl: string;
installed: boolean;
hasUpdate: boolean;
obsolete: boolean;
}
export interface ISource { export interface ISource {
id: string; id: string;