Use gql for "extensions"
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user