Extract "Extensions" types, constants, utils into separate files

This commit is contained in:
schroda
2024-11-04 01:21:50 +01:00
parent 71292a114a
commit 63caf74e36
11 changed files with 253 additions and 209 deletions

View File

@@ -21,7 +21,7 @@ import Tooltip from '@mui/material/Tooltip';
import ListItem from '@mui/material/ListItem';
import { useTranslation } from 'react-i18next';
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[]) {
secondList.forEach((item) => {

View 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',
};

View 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;

View 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,
};
}

View File

@@ -18,8 +18,20 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { makeToast } from '@/modules/core/utils/Toast.ts';
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
import { TExtension } from '@/modules/extension/services/Extensions.ts';
import { TranslationKey } from '@/Base.types.ts';
import {
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 {
extension: TExtension;
@@ -27,69 +39,6 @@ interface IProps {
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) {
const { t } = useTranslation();

View File

@@ -18,16 +18,8 @@ import Typography from '@mui/material/Typography';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
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 {
ExtensionGroupState,
GroupedExtensions,
GroupedExtensionsResult,
isExtensionStateOrLanguage,
TExtension,
translateExtensionLanguage,
} from '@/modules/extension/services/Extensions.ts';
import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx';
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
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 { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
import {
getExtensionsInfo,
isExtensionStateOrLanguage,
translateExtensionLanguage,
} from '@/modules/extension/Extensions.utils.ts';
const LANGUAGE = 0;
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 }) {
const { t } = useTranslation();
const { setAction } = useContext(NavBarContext);

View File

@@ -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);

View File

@@ -17,7 +17,6 @@ import Box from '@mui/material/Box';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
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 { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx';
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 { BaseMangaGrid } from '@/modules/manga/components/BaseMangaGrid.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 SourceToLoadingStateMap = Map<string, SourceLoadingState>;

View File

@@ -17,8 +17,8 @@ import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { requestManager } from '@/lib/requests/RequestManager.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 { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
export type TMigratableSource = NonNullable<GetMigratableSourcesQuery['mangas']['nodes'][number]['source']> & {
mangaCount: number;

View File

@@ -18,11 +18,11 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Stack from '@mui/material/Stack';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { translateExtensionLanguage } from '@/modules/extension/services/Extensions.ts';
import { SourceContentType } from '@/modules/source/screens/SourceMangas.tsx';
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
import { GetSourcesListQuery } from '@/lib/graphql/generated/graphql.ts';
import { MediaQuery } from '@/modules/core/utils/MediaQuery.tsx';
import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
interface IProps {
source: GetSourcesListQuery['sources']['nodes'][number];

View File

@@ -21,7 +21,6 @@ import {
langSortCmp,
DefaultLanguage,
} from '@/modules/core/utils/Languages.ts';
import { translateExtensionLanguage } from '@/modules/extension/services/Extensions.ts';
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
import { SourceCard } from '@/modules/source/components/SourceCard.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 { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
function sourceToLangList(sources: Pick<SourceType, 'id' | 'lang'>[]) {
const result = new Set<string>();