Move extension files into new folder
This commit is contained in:
237
src/modules/extension/components/ExtensionCard.tsx
Normal file
237
src/modules/extension/components/ExtensionCard.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* 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 { useState } from 'react';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Button from '@mui/material/Button';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { TExtension } from '@/modules/extension/services/Extensions.ts';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
interface IProps {
|
||||
extension: TExtension;
|
||||
handleUpdate: () => void;
|
||||
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();
|
||||
|
||||
const {
|
||||
extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw, repo },
|
||||
handleUpdate,
|
||||
showSourceRepo,
|
||||
} = props;
|
||||
const [installedState, setInstalledState] = useState<InstalledStates>(
|
||||
getInstalledState(isInstalled, isObsolete, hasUpdate),
|
||||
);
|
||||
|
||||
const langPress = lang === 'all' ? t('extension.language.all') : lang.toUpperCase();
|
||||
|
||||
const requestExtensionAction = async (action: ExtensionAction): Promise<void> => {
|
||||
const nextAction = EXTENSION_ACTION_TO_NEXT_ACTION_MAP[action];
|
||||
const state = EXTENSION_ACTION_TO_STATE_MAP[action];
|
||||
|
||||
try {
|
||||
setInstalledState(state);
|
||||
switch (action) {
|
||||
case ExtensionAction.INSTALL:
|
||||
await requestManager.updateExtension(pkgName, { install: true, isObsolete }).response;
|
||||
break;
|
||||
case ExtensionAction.UNINSTALL:
|
||||
await requestManager.updateExtension(pkgName, { uninstall: true, isObsolete }).response;
|
||||
break;
|
||||
case ExtensionAction.UPDATE:
|
||||
await requestManager.updateExtension(pkgName, { update: true, isObsolete }).response;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected ExtensionAction "${action}"`);
|
||||
}
|
||||
setInstalledState(nextAction);
|
||||
|
||||
handleUpdate();
|
||||
} catch (e) {
|
||||
setInstalledState(getInstalledState(isInstalled, isObsolete, hasUpdate));
|
||||
makeToast(t(EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[action]), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
function handleButtonClick() {
|
||||
switch (installedState) {
|
||||
case ExtensionAction.INSTALL:
|
||||
case ExtensionAction.UPDATE:
|
||||
case ExtensionAction.UNINSTALL:
|
||||
requestExtensionAction(installedState).catch(
|
||||
defaultPromiseErrorHandler(`ExtensionCard:handleButtonClick(${installedState})`),
|
||||
);
|
||||
break;
|
||||
case ExtensionState.OBSOLETE:
|
||||
requestExtensionAction(ExtensionAction.UNINSTALL).catch(
|
||||
defaultPromiseErrorHandler(`ExtensionCard:handleButtonClick(${installedState})`),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1.5,
|
||||
'&:last-child': {
|
||||
paddingBottom: 1.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
sx={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
flex: '0 0 auto',
|
||||
background: 'transparent',
|
||||
}}
|
||||
alt={name}
|
||||
>
|
||||
<SpinnerImage
|
||||
spinnerStyle={{ small: true }}
|
||||
imgStyle={{ objectFit: 'cover', width: '100%', height: '100%' }}
|
||||
alt={name}
|
||||
src={requestManager.getValidImgUrlFor(iconUrl)}
|
||||
/>
|
||||
</Avatar>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
wordBreak: 'break-word',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="h3">
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{langPress} {versionName}
|
||||
{isNsfw && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="error"
|
||||
sx={{
|
||||
display: 'inline',
|
||||
}}
|
||||
>
|
||||
{' 18+'}
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
{showSourceRepo && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{repo}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{ color: installedState === InstalledState.OBSOLETE ? 'red' : 'inherit', flexShrink: 0 }}
|
||||
onClick={() => handleButtonClick()}
|
||||
>
|
||||
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
319
src/modules/extension/screens/Extensions.tsx
Normal file
319
src/modules/extension/screens/Extensions.tsx
Normal file
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* 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 { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { fromEvent } from 'file-selector';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { extensionDefaultLangs, DefaultLanguage, langSortCmp } from '@/lib/Languages.tsx';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import {
|
||||
ExtensionState,
|
||||
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 '@/lib/ui/Toast.ts';
|
||||
import { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx';
|
||||
import { ExtensionCard } from '@/modules/extension/components/ExtensionCard.tsx';
|
||||
import { NavBarContext } from '@/components/context/NavbarContext.tsx';
|
||||
import { StyledGroupedVirtuoso } from '@/modules/core/components/virtuoso/StyledGroupedVirtuoso.tsx';
|
||||
import { StyledGroupHeader } from '@/modules/core/components/virtuoso/StyledGroupHeader.tsx';
|
||||
import { StyledGroupItemWrapper } from '@/modules/core/components/virtuoso/StyledGroupItemWrapper.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
|
||||
const LANGUAGE = 0;
|
||||
const EXTENSIONS = 1;
|
||||
|
||||
function getExtensionsInfo(extensions: TExtension[]): {
|
||||
allLangs: string[];
|
||||
groupedExtensions: GroupedExtensionsResult;
|
||||
} {
|
||||
const allLangs: string[] = [];
|
||||
const sortedExtensions: GroupedExtensions = {
|
||||
[ExtensionState.OBSOLETE]: [],
|
||||
[ExtensionState.INSTALLED]: [],
|
||||
[ExtensionState.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[ExtensionState.UPDATE_PENDING].push(extension);
|
||||
return;
|
||||
}
|
||||
if (extension.isObsolete) {
|
||||
sortedExtensions[ExtensionState.OBSOLETE].push(extension);
|
||||
return;
|
||||
}
|
||||
|
||||
sortedExtensions[ExtensionState.INSTALLED].push(extension);
|
||||
} else {
|
||||
sortedExtensions[extension.lang].push(extension);
|
||||
}
|
||||
});
|
||||
|
||||
allLangs.sort(langSortCmp);
|
||||
const result: GroupedExtensionsResult<ExtensionState | DefaultLanguage | string> = [
|
||||
[ExtensionState.OBSOLETE, sortedExtensions[ExtensionState.OBSOLETE]],
|
||||
[ExtensionState.UPDATE_PENDING, sortedExtensions[ExtensionState.UPDATE_PENDING]],
|
||||
[ExtensionState.INSTALLED, sortedExtensions[ExtensionState.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);
|
||||
|
||||
const {
|
||||
data: serverSettingsData,
|
||||
loading: areServerSettingsLoading,
|
||||
error: serverSettingsError,
|
||||
refetch: refetchServerSettings,
|
||||
} = requestManager.useGetServerSettings({ notifyOnNetworkStatusChange: true });
|
||||
const areReposDefined = !!serverSettingsData?.settings.extensionRepos.length;
|
||||
const areMultipleReposInUse = (serverSettingsData?.settings.extensionRepos.length ?? 0) > 1;
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
|
||||
const [refetchExtensions, setRefetchExtensions] = useState({});
|
||||
const [fetchExtensions, { data, loading: areExtensionsLoading, error: extensionsError }] =
|
||||
requestManager.useExtensionListFetch();
|
||||
const allExtensions = data?.fetchExtensions?.extensions;
|
||||
|
||||
const handleExtensionUpdate = useCallback(() => setRefetchExtensions({}), []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchExtensions();
|
||||
}, [refetchExtensions]);
|
||||
|
||||
const filteredExtensions = useMemo(
|
||||
() =>
|
||||
(allExtensions ?? []).filter((ext) => {
|
||||
const nsfwFilter = showNsfw || !ext.isNsfw;
|
||||
if (!query) return nsfwFilter;
|
||||
return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase());
|
||||
}),
|
||||
[allExtensions, showNsfw, query],
|
||||
);
|
||||
|
||||
const { allLangs, groupedExtensions } = useMemo(() => getExtensionsInfo(filteredExtensions), [filteredExtensions]);
|
||||
|
||||
const filteredGroupedExtensions = useMemo(
|
||||
() =>
|
||||
groupedExtensions
|
||||
.filter((group) => group[EXTENSIONS].length > 0)
|
||||
.filter((group) => isExtensionStateOrLanguage(group[LANGUAGE]) || shownLangs.includes(group[LANGUAGE])),
|
||||
[shownLangs, groupedExtensions],
|
||||
);
|
||||
|
||||
const groupCounts = useMemo(
|
||||
() => filteredGroupedExtensions.map((extensionGroup) => extensionGroup[EXTENSIONS].length),
|
||||
[filteredGroupedExtensions],
|
||||
);
|
||||
const visibleExtensions = useMemo(
|
||||
() => filteredGroupedExtensions.map(([, extensions]) => extensions).flat(1),
|
||||
[filteredGroupedExtensions],
|
||||
);
|
||||
|
||||
const submitExternalExtension = (file: File) => {
|
||||
if (file.name.toLowerCase().endsWith('apk')) {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = '';
|
||||
}
|
||||
|
||||
makeToast(t('extension.label.installing_file'), 'info');
|
||||
requestManager
|
||||
.installExternalExtension(file)
|
||||
.response.then(() => {
|
||||
handleExtensionUpdate();
|
||||
makeToast(t('extension.label.installed_successfully'), 'success');
|
||||
})
|
||||
.catch(() => makeToast(t('extension.label.installation_failed'), 'error'));
|
||||
} else {
|
||||
makeToast(t('global.error.label.invalid_file_type'), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setAction(
|
||||
<>
|
||||
<AppbarSearch />
|
||||
<Tooltip title={t('extension.action.label.install_external')}>
|
||||
<IconButton onClick={() => inputRef.current?.click()} size="large" color="inherit">
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<LangSelect shownLangs={shownLangs} setShownLangs={setShownLangs} allLangs={allLangs} />
|
||||
</>,
|
||||
);
|
||||
|
||||
return () => {
|
||||
setAction(null);
|
||||
};
|
||||
}, [t, shownLangs, allLangs]);
|
||||
|
||||
useEffect(() => {
|
||||
const dropHandler = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const files = await fromEvent(e);
|
||||
submitExternalExtension(files[0] as File);
|
||||
};
|
||||
|
||||
const dragOverHandler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
document.addEventListener('drop', dropHandler);
|
||||
document.addEventListener('dragover', dragOverHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('drop', dropHandler);
|
||||
document.removeEventListener('dragover', dragOverHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const FileInputComponent = useMemo(
|
||||
() => (
|
||||
<input
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
ref={inputRef}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
submitExternalExtension(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const isLoading = areServerSettingsLoading || areExtensionsLoading;
|
||||
const error = serverSettingsError ?? extensionsError;
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingPlaceholder />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyViewAbsoluteCentered
|
||||
message={t('global.error.label.failed_to_load_data')}
|
||||
messageExtra={error.message}
|
||||
retry={() => {
|
||||
if (serverSettingsError) {
|
||||
refetchServerSettings().catch(defaultPromiseErrorHandler('Extensions::refetchServerSettings'));
|
||||
}
|
||||
|
||||
if (extensionsError) {
|
||||
fetchExtensions().catch(defaultPromiseErrorHandler('Extensions::refetchExtensions'));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const showAddRepoInfo = !allExtensions?.length && !areReposDefined;
|
||||
if (showAddRepoInfo) {
|
||||
return (
|
||||
<>
|
||||
{FileInputComponent}
|
||||
<Stack
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
rowGap: '10px',
|
||||
paddingTop: '20px',
|
||||
}}
|
||||
>
|
||||
<Typography>{t('extension.label.add_repository_info')}</Typography>
|
||||
<Button component={Link} variant="contained" to="/settings/browseSettings">
|
||||
{t('settings.title')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{FileInputComponent}
|
||||
<StyledGroupedVirtuoso
|
||||
heightToSubtract={tabsMenuHeight}
|
||||
overscan={window.innerHeight * 0.5}
|
||||
groupCounts={groupCounts}
|
||||
groupContent={(index) => {
|
||||
const [groupName] = filteredGroupedExtensions[index];
|
||||
|
||||
return (
|
||||
<StyledGroupHeader key={groupName} variant="h5" component="h2" isFirstItem={index === 0}>
|
||||
{translateExtensionLanguage(groupName)}
|
||||
</StyledGroupHeader>
|
||||
);
|
||||
}}
|
||||
itemContent={(index) => {
|
||||
const item = visibleExtensions[index];
|
||||
|
||||
return (
|
||||
<StyledGroupItemWrapper
|
||||
key={`${item.pkgName}_${item.isInstalled}_${item.isObsolete}_${item.hasUpdate}`}
|
||||
>
|
||||
<ExtensionCard
|
||||
extension={item}
|
||||
handleUpdate={handleExtensionUpdate}
|
||||
showSourceRepo={areMultipleReposInUse}
|
||||
/>
|
||||
</StyledGroupItemWrapper>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
71
src/modules/extension/services/Extensions.ts
Normal file
71
src/modules/extension/services/Extensions.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 '@/lib/Languages.tsx';
|
||||
import { ExtensionType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
export enum ExtensionState {
|
||||
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 ExtensionState]: TExtension[];
|
||||
};
|
||||
|
||||
export type GroupedByLanguage = {
|
||||
[language in DefaultLanguage]: TExtension[];
|
||||
} & {
|
||||
[language: string]: TExtension[];
|
||||
};
|
||||
|
||||
export type GroupedExtensions = GroupedByExtensionState & GroupedByLanguage;
|
||||
|
||||
export const extensionLanguageToTranslationKey: { [state in ExtensionState | DefaultLanguage]: TranslationKey } = {
|
||||
[ExtensionState.INSTALLED]: 'extension.state.label.installed',
|
||||
[ExtensionState.UPDATE_PENDING]: 'extension.state.label.update_pending',
|
||||
[ExtensionState.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 =>
|
||||
[
|
||||
ExtensionState.INSTALLED,
|
||||
ExtensionState.UPDATE_PENDING,
|
||||
ExtensionState.OBSOLETE,
|
||||
DefaultLanguage.ALL,
|
||||
DefaultLanguage.OTHER,
|
||||
DefaultLanguage.LOCAL_SOURCE,
|
||||
].includes(languageCode as ExtensionState | DefaultLanguage);
|
||||
|
||||
export const translateExtensionLanguage = (languageCode: string): string =>
|
||||
isExtensionStateOrLanguage(languageCode)
|
||||
? t(extensionLanguageToTranslationKey[languageCode as ExtensionState | DefaultLanguage])
|
||||
: langCodeToName(languageCode);
|
||||
Reference in New Issue
Block a user