Extract "Extensions" types, constants, utils into separate files
This commit is contained in:
@@ -21,7 +21,7 @@ import Tooltip from '@mui/material/Tooltip';
|
|||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cloneObject } from '@/util/cloneObject.tsx';
|
import { cloneObject } from '@/util/cloneObject.tsx';
|
||||||
import { translateExtensionLanguage } from '@/modules/extension/services/Extensions.ts';
|
import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
|
||||||
|
|
||||||
function removeAll(firstList: any[], secondList: any[]) {
|
function removeAll(firstList: any[], secondList: any[]) {
|
||||||
secondList.forEach((item) => {
|
secondList.forEach((item) => {
|
||||||
|
|||||||
56
src/modules/extension/Extensions.constants.ts
Normal file
56
src/modules/extension/Extensions.constants.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExtensionAction,
|
||||||
|
ExtensionGroupState,
|
||||||
|
ExtensionState,
|
||||||
|
InstalledState,
|
||||||
|
InstalledStates,
|
||||||
|
} from '@/modules/extension/Extensions.types.ts';
|
||||||
|
import { TranslationKey } from '@/Base.types.ts';
|
||||||
|
import { DefaultLanguage } from '@/modules/core/utils/Languages.ts';
|
||||||
|
|
||||||
|
export const EXTENSION_ACTION_TO_STATE_MAP: { [action in ExtensionAction]: ExtensionState } = {
|
||||||
|
[ExtensionAction.UPDATE]: ExtensionState.UPDATING,
|
||||||
|
[ExtensionAction.UNINSTALL]: ExtensionState.UNINSTALLING,
|
||||||
|
[ExtensionAction.INSTALL]: ExtensionState.INSTALLING,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const EXTENSION_ACTION_TO_NEXT_ACTION_MAP: { [action in ExtensionAction]: ExtensionAction } = {
|
||||||
|
[ExtensionAction.UPDATE]: ExtensionAction.UNINSTALL,
|
||||||
|
[ExtensionAction.UNINSTALL]: ExtensionAction.INSTALL,
|
||||||
|
[ExtensionAction.INSTALL]: ExtensionAction.UNINSTALL,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const INSTALLED_STATE_TO_TRANSLATION_KEY_MAP: { [installedState in InstalledStates]: TranslationKey } = {
|
||||||
|
[InstalledState.UNINSTALL]: 'extension.action.label.uninstall',
|
||||||
|
[InstalledState.INSTALL]: 'extension.action.label.install',
|
||||||
|
[InstalledState.UPDATE]: 'extension.action.label.update',
|
||||||
|
[InstalledState.OBSOLETE]: 'extension.state.label.obsolete',
|
||||||
|
[InstalledState.UPDATING]: 'extension.state.label.updating',
|
||||||
|
[InstalledState.UNINSTALLING]: 'extension.state.label.uninstalling',
|
||||||
|
[InstalledState.INSTALLING]: 'extension.state.label.installing',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP: {
|
||||||
|
[action in ExtensionAction]: TranslationKey;
|
||||||
|
} = {
|
||||||
|
[ExtensionAction.UPDATE]: 'extension.label.update_failed',
|
||||||
|
[ExtensionAction.INSTALL]: 'extension.label.installation_failed',
|
||||||
|
[ExtensionAction.UNINSTALL]: 'extension.label.uninstallation_failed',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extensionLanguageToTranslationKey: { [state in ExtensionGroupState | DefaultLanguage]: TranslationKey } = {
|
||||||
|
[ExtensionGroupState.INSTALLED]: 'extension.state.label.installed',
|
||||||
|
[ExtensionGroupState.UPDATE_PENDING]: 'extension.state.label.update_pending',
|
||||||
|
[ExtensionGroupState.OBSOLETE]: 'extension.state.label.obsolete',
|
||||||
|
[DefaultLanguage.ALL]: 'extension.language.all',
|
||||||
|
[DefaultLanguage.OTHER]: 'extension.language.other',
|
||||||
|
[DefaultLanguage.LOCAL_SOURCE]: 'extension.language.other',
|
||||||
|
};
|
||||||
62
src/modules/extension/Extensions.types.ts
Normal file
62
src/modules/extension/Extensions.types.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DefaultLanguage } from '@/modules/core/utils/Languages.ts';
|
||||||
|
import { ExtensionType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
|
||||||
|
export enum ExtensionAction {
|
||||||
|
UPDATE = 'UPDATE',
|
||||||
|
UNINSTALL = 'UNINSTALL',
|
||||||
|
INSTALL = 'INSTALL',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ExtensionState {
|
||||||
|
OBSOLETE = 'OBSOLETE',
|
||||||
|
UPDATING = 'UPDATING',
|
||||||
|
UNINSTALLING = 'UNINSTALLING',
|
||||||
|
INSTALLING = 'INSTALLING',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstalledStates = ExtensionAction | ExtensionState;
|
||||||
|
|
||||||
|
export const InstalledState = { ...ExtensionAction, ...ExtensionState } as const;
|
||||||
|
|
||||||
|
export enum ExtensionGroupState {
|
||||||
|
INSTALLED = 'INSTALLED',
|
||||||
|
UPDATE_PENDING = 'UPDATE_PENDING',
|
||||||
|
OBSOLETE = 'OBSOLETE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TExtension = Pick<
|
||||||
|
ExtensionType,
|
||||||
|
| 'pkgName'
|
||||||
|
| 'name'
|
||||||
|
| 'lang'
|
||||||
|
| 'versionCode'
|
||||||
|
| 'versionName'
|
||||||
|
| 'iconUrl'
|
||||||
|
| 'repo'
|
||||||
|
| 'isNsfw'
|
||||||
|
| 'isInstalled'
|
||||||
|
| 'isObsolete'
|
||||||
|
| 'hasUpdate'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GroupedExtensionsResult<KEY extends string = string> = [KEY, TExtension[]][];
|
||||||
|
|
||||||
|
export type GroupedByExtensionState = {
|
||||||
|
[state in ExtensionGroupState]: TExtension[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupedByLanguage = {
|
||||||
|
[language in DefaultLanguage]: TExtension[];
|
||||||
|
} & {
|
||||||
|
[language: string]: TExtension[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GroupedExtensions = GroupedByExtensionState & GroupedByLanguage;
|
||||||
110
src/modules/extension/Extensions.utils.ts
Normal file
110
src/modules/extension/Extensions.utils.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import {
|
||||||
|
ExtensionAction,
|
||||||
|
ExtensionGroupState,
|
||||||
|
ExtensionState,
|
||||||
|
GroupedExtensions,
|
||||||
|
GroupedExtensionsResult,
|
||||||
|
InstalledState,
|
||||||
|
TExtension,
|
||||||
|
} from '@/modules/extension/Extensions.types.ts';
|
||||||
|
import { DefaultLanguage, langCodeToName, langSortCmp } from '@/modules/core/utils/Languages.ts';
|
||||||
|
import { extensionLanguageToTranslationKey } from '@/modules/extension/Extensions.constants.ts';
|
||||||
|
|
||||||
|
export const getInstalledState = (
|
||||||
|
isInstalled: boolean,
|
||||||
|
isObsolete: boolean,
|
||||||
|
hasUpdate: boolean,
|
||||||
|
): ExtensionAction | ExtensionState.OBSOLETE => {
|
||||||
|
if (isObsolete) {
|
||||||
|
return InstalledState.OBSOLETE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUpdate) {
|
||||||
|
return InstalledState.UPDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInstalled ? InstalledState.UNINSTALL : InstalledState.INSTALL;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isExtensionStateOrLanguage = (languageCode: string): boolean =>
|
||||||
|
[
|
||||||
|
ExtensionGroupState.INSTALLED,
|
||||||
|
ExtensionGroupState.UPDATE_PENDING,
|
||||||
|
ExtensionGroupState.OBSOLETE,
|
||||||
|
DefaultLanguage.ALL,
|
||||||
|
DefaultLanguage.OTHER,
|
||||||
|
DefaultLanguage.LOCAL_SOURCE,
|
||||||
|
].includes(languageCode as ExtensionGroupState | DefaultLanguage);
|
||||||
|
|
||||||
|
export const translateExtensionLanguage = (languageCode: string): string =>
|
||||||
|
isExtensionStateOrLanguage(languageCode)
|
||||||
|
? t(extensionLanguageToTranslationKey[languageCode as ExtensionGroupState | DefaultLanguage])
|
||||||
|
: langCodeToName(languageCode);
|
||||||
|
|
||||||
|
export function getExtensionsInfo(extensions: TExtension[]): {
|
||||||
|
allLangs: string[];
|
||||||
|
groupedExtensions: GroupedExtensionsResult;
|
||||||
|
} {
|
||||||
|
const allLangs: string[] = [];
|
||||||
|
const sortedExtensions: GroupedExtensions = {
|
||||||
|
[ExtensionGroupState.OBSOLETE]: [],
|
||||||
|
[ExtensionGroupState.INSTALLED]: [],
|
||||||
|
[ExtensionGroupState.UPDATE_PENDING]: [],
|
||||||
|
[DefaultLanguage.ALL]: [],
|
||||||
|
[DefaultLanguage.OTHER]: [],
|
||||||
|
[DefaultLanguage.LOCAL_SOURCE]: [],
|
||||||
|
};
|
||||||
|
extensions.forEach((extension) => {
|
||||||
|
if (sortedExtensions[extension.lang] === undefined) {
|
||||||
|
sortedExtensions[extension.lang] = [];
|
||||||
|
if (extension.lang !== 'all') {
|
||||||
|
allLangs.push(extension.lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (extension.isInstalled) {
|
||||||
|
if (extension.hasUpdate) {
|
||||||
|
sortedExtensions[ExtensionGroupState.UPDATE_PENDING].push(extension);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (extension.isObsolete) {
|
||||||
|
sortedExtensions[ExtensionGroupState.OBSOLETE].push(extension);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedExtensions[ExtensionGroupState.INSTALLED].push(extension);
|
||||||
|
} else {
|
||||||
|
sortedExtensions[extension.lang].push(extension);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
allLangs.sort(langSortCmp);
|
||||||
|
const result: GroupedExtensionsResult<ExtensionGroupState | DefaultLanguage | string> = [
|
||||||
|
[ExtensionGroupState.OBSOLETE, sortedExtensions[ExtensionGroupState.OBSOLETE]],
|
||||||
|
[ExtensionGroupState.UPDATE_PENDING, sortedExtensions[ExtensionGroupState.UPDATE_PENDING]],
|
||||||
|
[ExtensionGroupState.INSTALLED, sortedExtensions[ExtensionGroupState.INSTALLED]],
|
||||||
|
[DefaultLanguage.ALL, sortedExtensions[DefaultLanguage.ALL]],
|
||||||
|
[DefaultLanguage.OTHER, sortedExtensions[DefaultLanguage.OTHER]],
|
||||||
|
[DefaultLanguage.LOCAL_SOURCE, sortedExtensions[DefaultLanguage.LOCAL_SOURCE]],
|
||||||
|
];
|
||||||
|
|
||||||
|
const langExt: GroupedExtensionsResult = allLangs.map((lang) => [lang, sortedExtensions[lang]]);
|
||||||
|
const groupedExtensions = result.concat(langExt);
|
||||||
|
|
||||||
|
groupedExtensions.forEach(([, groupedExtensionList]) =>
|
||||||
|
groupedExtensionList.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allLangs,
|
||||||
|
groupedExtensions,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -18,8 +18,20 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
|
|||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||||
import { TExtension } from '@/modules/extension/services/Extensions.ts';
|
import {
|
||||||
import { TranslationKey } from '@/Base.types.ts';
|
ExtensionAction,
|
||||||
|
ExtensionState,
|
||||||
|
InstalledState,
|
||||||
|
InstalledStates,
|
||||||
|
TExtension,
|
||||||
|
} from '@/modules/extension/Extensions.types.ts';
|
||||||
|
import {
|
||||||
|
EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP,
|
||||||
|
EXTENSION_ACTION_TO_NEXT_ACTION_MAP,
|
||||||
|
EXTENSION_ACTION_TO_STATE_MAP,
|
||||||
|
INSTALLED_STATE_TO_TRANSLATION_KEY_MAP,
|
||||||
|
} from '@/modules/extension/Extensions.constants.ts';
|
||||||
|
import { getInstalledState } from '@/modules/extension/Extensions.utils.ts';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
extension: TExtension;
|
extension: TExtension;
|
||||||
@@ -27,69 +39,6 @@ interface IProps {
|
|||||||
showSourceRepo: boolean;
|
showSourceRepo: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ExtensionAction {
|
|
||||||
UPDATE = 'UPDATE',
|
|
||||||
UNINSTALL = 'UNINSTALL',
|
|
||||||
INSTALL = 'INSTALL',
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ExtensionState {
|
|
||||||
OBSOLETE = 'OBSOLETE',
|
|
||||||
UPDATING = 'UPDATING',
|
|
||||||
UNINSTALLING = 'UNINSTALLING',
|
|
||||||
INSTALLING = 'INSTALLING',
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstalledStates = ExtensionAction | ExtensionState;
|
|
||||||
|
|
||||||
const InstalledState = { ...ExtensionAction, ...ExtensionState } as const;
|
|
||||||
|
|
||||||
const EXTENSION_ACTION_TO_STATE_MAP: { [action in ExtensionAction]: ExtensionState } = {
|
|
||||||
[ExtensionAction.UPDATE]: ExtensionState.UPDATING,
|
|
||||||
[ExtensionAction.UNINSTALL]: ExtensionState.UNINSTALLING,
|
|
||||||
[ExtensionAction.INSTALL]: ExtensionState.INSTALLING,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const EXTENSION_ACTION_TO_NEXT_ACTION_MAP: { [action in ExtensionAction]: ExtensionAction } = {
|
|
||||||
[ExtensionAction.UPDATE]: ExtensionAction.UNINSTALL,
|
|
||||||
[ExtensionAction.UNINSTALL]: ExtensionAction.INSTALL,
|
|
||||||
[ExtensionAction.INSTALL]: ExtensionAction.UNINSTALL,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const INSTALLED_STATE_TO_TRANSLATION_KEY_MAP: { [installedState in InstalledStates]: TranslationKey } = {
|
|
||||||
[InstalledState.UNINSTALL]: 'extension.action.label.uninstall',
|
|
||||||
[InstalledState.INSTALL]: 'extension.action.label.install',
|
|
||||||
[InstalledState.UPDATE]: 'extension.action.label.update',
|
|
||||||
[InstalledState.OBSOLETE]: 'extension.state.label.obsolete',
|
|
||||||
[InstalledState.UPDATING]: 'extension.state.label.updating',
|
|
||||||
[InstalledState.UNINSTALLING]: 'extension.state.label.uninstalling',
|
|
||||||
[InstalledState.INSTALLING]: 'extension.state.label.installing',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP: {
|
|
||||||
[action in ExtensionAction]: TranslationKey;
|
|
||||||
} = {
|
|
||||||
[ExtensionAction.UPDATE]: 'extension.label.update_failed',
|
|
||||||
[ExtensionAction.INSTALL]: 'extension.label.installation_failed',
|
|
||||||
[ExtensionAction.UNINSTALL]: 'extension.label.uninstallation_failed',
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInstalledState = (
|
|
||||||
isInstalled: boolean,
|
|
||||||
isObsolete: boolean,
|
|
||||||
hasUpdate: boolean,
|
|
||||||
): ExtensionAction | ExtensionState.OBSOLETE => {
|
|
||||||
if (isObsolete) {
|
|
||||||
return InstalledState.OBSOLETE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasUpdate) {
|
|
||||||
return InstalledState.UPDATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isInstalled ? InstalledState.UNINSTALL : InstalledState.INSTALL;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ExtensionCard(props: IProps) {
|
export function ExtensionCard(props: IProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|||||||
@@ -18,16 +18,8 @@ import Typography from '@mui/material/Typography';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { extensionDefaultLangs, DefaultLanguage, langSortCmp } from '@/modules/core/utils/Languages.ts';
|
import { extensionDefaultLangs } from '@/modules/core/utils/Languages.ts';
|
||||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||||
import {
|
|
||||||
ExtensionGroupState,
|
|
||||||
GroupedExtensions,
|
|
||||||
GroupedExtensionsResult,
|
|
||||||
isExtensionStateOrLanguage,
|
|
||||||
TExtension,
|
|
||||||
translateExtensionLanguage,
|
|
||||||
} from '@/modules/extension/services/Extensions.ts';
|
|
||||||
import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx';
|
import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx';
|
||||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||||
@@ -40,69 +32,15 @@ import { StyledGroupItemWrapper } from '@/modules/core/components/virtuoso/Style
|
|||||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||||
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
|
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
|
||||||
|
import {
|
||||||
|
getExtensionsInfo,
|
||||||
|
isExtensionStateOrLanguage,
|
||||||
|
translateExtensionLanguage,
|
||||||
|
} from '@/modules/extension/Extensions.utils.ts';
|
||||||
|
|
||||||
const LANGUAGE = 0;
|
const LANGUAGE = 0;
|
||||||
const EXTENSIONS = 1;
|
const EXTENSIONS = 1;
|
||||||
|
|
||||||
function getExtensionsInfo(extensions: TExtension[]): {
|
|
||||||
allLangs: string[];
|
|
||||||
groupedExtensions: GroupedExtensionsResult;
|
|
||||||
} {
|
|
||||||
const allLangs: string[] = [];
|
|
||||||
const sortedExtensions: GroupedExtensions = {
|
|
||||||
[ExtensionGroupState.OBSOLETE]: [],
|
|
||||||
[ExtensionGroupState.INSTALLED]: [],
|
|
||||||
[ExtensionGroupState.UPDATE_PENDING]: [],
|
|
||||||
[DefaultLanguage.ALL]: [],
|
|
||||||
[DefaultLanguage.OTHER]: [],
|
|
||||||
[DefaultLanguage.LOCAL_SOURCE]: [],
|
|
||||||
};
|
|
||||||
extensions.forEach((extension) => {
|
|
||||||
if (sortedExtensions[extension.lang] === undefined) {
|
|
||||||
sortedExtensions[extension.lang] = [];
|
|
||||||
if (extension.lang !== 'all') {
|
|
||||||
allLangs.push(extension.lang);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (extension.isInstalled) {
|
|
||||||
if (extension.hasUpdate) {
|
|
||||||
sortedExtensions[ExtensionGroupState.UPDATE_PENDING].push(extension);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (extension.isObsolete) {
|
|
||||||
sortedExtensions[ExtensionGroupState.OBSOLETE].push(extension);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sortedExtensions[ExtensionGroupState.INSTALLED].push(extension);
|
|
||||||
} else {
|
|
||||||
sortedExtensions[extension.lang].push(extension);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
allLangs.sort(langSortCmp);
|
|
||||||
const result: GroupedExtensionsResult<ExtensionGroupState | DefaultLanguage | string> = [
|
|
||||||
[ExtensionGroupState.OBSOLETE, sortedExtensions[ExtensionGroupState.OBSOLETE]],
|
|
||||||
[ExtensionGroupState.UPDATE_PENDING, sortedExtensions[ExtensionGroupState.UPDATE_PENDING]],
|
|
||||||
[ExtensionGroupState.INSTALLED, sortedExtensions[ExtensionGroupState.INSTALLED]],
|
|
||||||
[DefaultLanguage.ALL, sortedExtensions[DefaultLanguage.ALL]],
|
|
||||||
[DefaultLanguage.OTHER, sortedExtensions[DefaultLanguage.OTHER]],
|
|
||||||
[DefaultLanguage.LOCAL_SOURCE, sortedExtensions[DefaultLanguage.LOCAL_SOURCE]],
|
|
||||||
];
|
|
||||||
|
|
||||||
const langExt: GroupedExtensionsResult = allLangs.map((lang) => [lang, sortedExtensions[lang]]);
|
|
||||||
const groupedExtensions = result.concat(langExt);
|
|
||||||
|
|
||||||
groupedExtensions.forEach(([, groupedExtensionList]) =>
|
|
||||||
groupedExtensionList.sort((a, b) => a.name.localeCompare(b.name)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
allLangs,
|
|
||||||
groupedExtensions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setAction } = useContext(NavBarContext);
|
const { setAction } = useContext(NavBarContext);
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { t } from 'i18next';
|
|
||||||
import { DefaultLanguage, langCodeToName } from '@/modules/core/utils/Languages.ts';
|
|
||||||
import { ExtensionType } from '@/lib/graphql/generated/graphql.ts';
|
|
||||||
import { TranslationKey } from '@/Base.types.ts';
|
|
||||||
|
|
||||||
export enum ExtensionGroupState {
|
|
||||||
INSTALLED = 'INSTALLED',
|
|
||||||
UPDATE_PENDING = 'UPDATE_PENDING',
|
|
||||||
OBSOLETE = 'OBSOLETE',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TExtension = Pick<
|
|
||||||
ExtensionType,
|
|
||||||
| 'pkgName'
|
|
||||||
| 'name'
|
|
||||||
| 'lang'
|
|
||||||
| 'versionCode'
|
|
||||||
| 'versionName'
|
|
||||||
| 'iconUrl'
|
|
||||||
| 'repo'
|
|
||||||
| 'isNsfw'
|
|
||||||
| 'isInstalled'
|
|
||||||
| 'isObsolete'
|
|
||||||
| 'hasUpdate'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type GroupedExtensionsResult<KEY extends string = string> = [KEY, TExtension[]][];
|
|
||||||
|
|
||||||
export type GroupedByExtensionState = {
|
|
||||||
[state in ExtensionGroupState]: TExtension[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GroupedByLanguage = {
|
|
||||||
[language in DefaultLanguage]: TExtension[];
|
|
||||||
} & {
|
|
||||||
[language: string]: TExtension[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GroupedExtensions = GroupedByExtensionState & GroupedByLanguage;
|
|
||||||
|
|
||||||
export const extensionLanguageToTranslationKey: { [state in ExtensionGroupState | DefaultLanguage]: TranslationKey } = {
|
|
||||||
[ExtensionGroupState.INSTALLED]: 'extension.state.label.installed',
|
|
||||||
[ExtensionGroupState.UPDATE_PENDING]: 'extension.state.label.update_pending',
|
|
||||||
[ExtensionGroupState.OBSOLETE]: 'extension.state.label.obsolete',
|
|
||||||
[DefaultLanguage.ALL]: 'extension.language.all',
|
|
||||||
[DefaultLanguage.OTHER]: 'extension.language.other',
|
|
||||||
[DefaultLanguage.LOCAL_SOURCE]: 'extension.language.other',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isExtensionStateOrLanguage = (languageCode: string): boolean =>
|
|
||||||
[
|
|
||||||
ExtensionGroupState.INSTALLED,
|
|
||||||
ExtensionGroupState.UPDATE_PENDING,
|
|
||||||
ExtensionGroupState.OBSOLETE,
|
|
||||||
DefaultLanguage.ALL,
|
|
||||||
DefaultLanguage.OTHER,
|
|
||||||
DefaultLanguage.LOCAL_SOURCE,
|
|
||||||
].includes(languageCode as ExtensionGroupState | DefaultLanguage);
|
|
||||||
|
|
||||||
export const translateExtensionLanguage = (languageCode: string): string =>
|
|
||||||
isExtensionStateOrLanguage(languageCode)
|
|
||||||
? t(extensionLanguageToTranslationKey[languageCode as ExtensionGroupState | DefaultLanguage])
|
|
||||||
: langCodeToName(languageCode);
|
|
||||||
@@ -17,7 +17,6 @@ import Box from '@mui/material/Box';
|
|||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||||
import { langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs } from '@/modules/core/utils/Languages.ts';
|
import { langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs } from '@/modules/core/utils/Languages.ts';
|
||||||
import { translateExtensionLanguage } from '@/modules/extension/services/Extensions.ts';
|
|
||||||
import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx';
|
import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx';
|
||||||
import { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx';
|
import { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx';
|
||||||
import { useDebounce } from '@/modules/core/hooks/useDebounce.ts';
|
import { useDebounce } from '@/modules/core/hooks/useDebounce.ts';
|
||||||
@@ -29,6 +28,7 @@ import { LoadingPlaceholder } from '@/modules/core/components/placeholder/Loadin
|
|||||||
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { BaseMangaGrid } from '@/modules/manga/components/BaseMangaGrid.tsx';
|
import { BaseMangaGrid } from '@/modules/manga/components/BaseMangaGrid.tsx';
|
||||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||||
|
import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
|
||||||
|
|
||||||
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean };
|
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean };
|
||||||
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
|
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import { Link } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
|
import { GetMigratableSourcesQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { translateExtensionLanguage } from '@/modules/extension/services/Extensions.ts';
|
|
||||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||||
|
import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
|
||||||
|
|
||||||
export type TMigratableSource = NonNullable<GetMigratableSourcesQuery['mangas']['nodes'][number]['source']> & {
|
export type TMigratableSource = NonNullable<GetMigratableSourcesQuery['mangas']['nodes'][number]['source']> & {
|
||||||
mangaCount: number;
|
mangaCount: number;
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { translateExtensionLanguage } from '@/modules/extension/services/Extensions.ts';
|
|
||||||
import { SourceContentType } from '@/modules/source/screens/SourceMangas.tsx';
|
import { SourceContentType } from '@/modules/source/screens/SourceMangas.tsx';
|
||||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||||
import { GetSourcesListQuery } from '@/lib/graphql/generated/graphql.ts';
|
import { GetSourcesListQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
|
||||||
|
import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
source: GetSourcesListQuery['sources']['nodes'][number];
|
source: GetSourcesListQuery['sources']['nodes'][number];
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
langSortCmp,
|
langSortCmp,
|
||||||
DefaultLanguage,
|
DefaultLanguage,
|
||||||
} from '@/modules/core/utils/Languages.ts';
|
} from '@/modules/core/utils/Languages.ts';
|
||||||
import { translateExtensionLanguage } from '@/modules/extension/services/Extensions.ts';
|
|
||||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||||
import { SourceCard } from '@/modules/source/components/SourceCard.tsx';
|
import { SourceCard } from '@/modules/source/components/SourceCard.tsx';
|
||||||
import { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx';
|
import { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx';
|
||||||
@@ -29,6 +28,7 @@ import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.t
|
|||||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||||
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
|
||||||
|
|
||||||
function sourceToLangList(sources: Pick<SourceType, 'id' | 'lang'>[]) {
|
function sourceToLangList(sources: Pick<SourceType, 'id' | 'lang'>[]) {
|
||||||
const result = new Set<string>();
|
const result = new Set<string>();
|
||||||
|
|||||||
Reference in New Issue
Block a user