Add option to update all updatable extensions

This commit is contained in:
schroda
2024-11-04 01:26:58 +01:00
parent 63caf74e36
commit e90d705610
7 changed files with 153 additions and 20 deletions

View File

@@ -247,16 +247,21 @@
"install": "Install",
"install_external": "Install external extension",
"uninstall": "Uninstall",
"update": "Update"
"update": "Update",
"update_all": "Update all"
}
},
"label": {
"add_repository_info": "You have to add a extension repository to be able to install extensions",
"installation_failed": "Could not install the extension",
"installed_successfully": "Extension installed",
"installation_failed_one": "Could not install the extension",
"installation_failed_other": "Could not install the extensions",
"installed_successfully_one": "Extension installed",
"installed_successfully_other": "Extensions installed",
"installing_file": "Installing extension file…",
"uninstallation_failed": "Could not uninstall the extension",
"update_failed": "Could not update the extension"
"uninstallation_failed_one": "Could not uninstall the extension",
"uninstallation_failed_other": "Could not uninstall the extensions",
"update_failed_one": "Could not update the extension",
"update_failed_other": "Could not update the extensions"
},
"language": {
"all": "All",

View File

@@ -183,6 +183,8 @@ import {
UpdateExtensionMutation,
UpdateExtensionMutationVariables,
UpdateExtensionPatchInput,
UpdateExtensionsMutation,
UpdateExtensionsMutationVariables,
UpdateLibraryMangasMutation,
UpdateLibraryMangasMutationVariables,
UpdateMangaCategoriesMutation,
@@ -221,6 +223,7 @@ import {
GET_EXTENSIONS_FETCH,
INSTALL_EXTERNAL_EXTENSION,
UPDATE_EXTENSION,
UPDATE_EXTENSIONS,
} from '@/lib/graphql/mutations/ExtensionMutation.ts';
import { GET_MIGRATABLE_SOURCES, GET_SOURCES_LIST } from '@/lib/graphql/queries/SourceQuery.ts';
import {
@@ -1427,6 +1430,72 @@ export class RequestManager {
return result;
}
public updateExtensions(
ids: string[],
{ isObsolete = false, ...patch }: UpdateExtensionPatchInput & { isObsolete?: boolean },
options?: MutationOptions<UpdateExtensionsMutation, UpdateExtensionsMutationVariables>,
): AbortableApolloMutationResponse<UpdateExtensionsMutation> {
const result = this.doRequest<UpdateExtensionsMutation, UpdateExtensionsMutationVariables>(
GQLMethod.MUTATION,
UPDATE_EXTENSIONS,
{ input: { ids, patch } },
options,
);
result.response.then((response) => {
if (response.errors) {
return;
}
this.graphQLClient.client.cache.evict({ fieldName: 'sources' });
const cachedExtensions = this.cache.getResponseFor<MutationResult<GetExtensionsFetchMutation>>(
EXTENSION_LIST_CACHE_KEY,
undefined,
);
if (!cachedExtensions || !cachedExtensions.data) {
return;
}
const updatedCachedExtensions: MutationResult<GetExtensionsFetchMutation> = {
...cachedExtensions,
data: {
...cachedExtensions.data,
fetchExtensions: {
...cachedExtensions.data.fetchExtensions,
extensions:
cachedExtensions.data.fetchExtensions?.extensions
.filter((extension) => {
if (!isObsolete) {
return true;
}
const isUpdatedExtension = ids.includes(extension.pkgName);
return !isUpdatedExtension;
})
.map((extension) => {
const isUpdatedExtension = ids.includes(extension.pkgName);
if (!isUpdatedExtension) {
return extension;
}
return {
...extension,
...(response.data?.updateExtensions?.extensions.find(
(updatedExtension) => updatedExtension.pkgName === extension.pkgName,
) ?? []),
};
}) ?? [],
},
},
};
this.cache.cacheResponse(EXTENSION_LIST_CACHE_KEY, undefined, updatedCachedExtensions);
});
return result;
}
public getExtensionIconUrl(extension: string): string {
return this.getValidImgUrlFor(`extension/icon/${extension}`);
}

View File

@@ -7,13 +7,14 @@
*/
import { styled } from '@mui/material/styles';
import Typography, { TypographyProps } from '@mui/material/Typography';
import { TypographyProps } from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import { shouldForwardProp } from '@/modules/core/utils/ShouldForwardProp.ts';
type StyledGroupHeaderProps = {
isFirstItem: boolean;
};
export const StyledGroupHeader = styled(Typography, {
export const StyledGroupHeader = styled(Stack, {
shouldForwardProp: shouldForwardProp<StyledGroupHeaderProps>(['isFirstItem']),
})<StyledGroupHeaderProps & TypographyProps>(({ theme, isFirstItem }) => ({
paddingLeft: theme.spacing(3),

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useState } from 'react';
import { useEffect, useState } from 'react';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Button from '@mui/material/Button';
@@ -37,6 +37,7 @@ interface IProps {
extension: TExtension;
handleUpdate: () => void;
showSourceRepo: boolean;
forcedState?: ExtensionState;
}
export function ExtensionCard(props: IProps) {
@@ -46,10 +47,16 @@ export function ExtensionCard(props: IProps) {
extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw, repo },
handleUpdate,
showSourceRepo,
forcedState,
} = props;
const [installedState, setInstalledState] = useState<InstalledStates>(
const [localInstalledState, setInstalledState] = useState<InstalledStates>(
getInstalledState(isInstalled, isObsolete, hasUpdate),
);
const installedState = forcedState ?? localInstalledState;
useEffect(() => {
setInstalledState(getInstalledState(isInstalled, isObsolete, hasUpdate));
}, [getInstalledState(isInstalled, isObsolete, hasUpdate)]);
const langPress = lang === 'all' ? t('extension.language.all') : lang.toUpperCase();
@@ -77,7 +84,7 @@ export function ExtensionCard(props: IProps) {
handleUpdate();
} catch (e) {
setInstalledState(getInstalledState(isInstalled, isObsolete, hasUpdate));
makeToast(t(EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[action]), 'error');
makeToast(t(EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[action], { count: 1 }), 'error');
}
};
@@ -175,7 +182,10 @@ export function ExtensionCard(props: IProps) {
</Box>
<Button
variant="outlined"
sx={{ color: installedState === InstalledState.OBSOLETE ? 'red' : 'inherit', flexShrink: 0 }}
sx={{
color: installedState === InstalledState.OBSOLETE ? 'red' : 'inherit',
flexShrink: 0,
}}
onClick={() => handleButtonClick()}
>
{t(INSTALLED_STATE_TO_TRANSLATION_KEY_MAP[installedState])}

View File

@@ -37,6 +37,8 @@ import {
isExtensionStateOrLanguage,
translateExtensionLanguage,
} from '@/modules/extension/Extensions.utils.ts';
import { ExtensionAction, ExtensionGroupState, ExtensionState } from '@/modules/extension/Extensions.types.ts';
import { EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP } from '@/modules/extension/Extensions.constants.ts';
const LANGUAGE = 0;
const EXTENSIONS = 1;
@@ -64,6 +66,8 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
requestManager.useExtensionListFetch();
const allExtensions = data?.fetchExtensions?.extensions;
const [updatingExtensionIds, setUpdatingExtensionIds] = useState<string[]>([]);
const handleExtensionUpdate = useCallback(() => setRefetchExtensions({}), []);
useEffect(() => {
@@ -235,11 +239,45 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
overscan={window.innerHeight * 0.5}
groupCounts={groupCounts}
groupContent={(index) => {
const [groupName] = filteredGroupedExtensions[index];
const [groupName, groupExtensions] = filteredGroupedExtensions[index];
const isUpdateGroup = groupName === ExtensionGroupState.UPDATE_PENDING;
return (
<StyledGroupHeader key={groupName} variant="h5" component="h2" isFirstItem={index === 0}>
<StyledGroupHeader
key={groupName}
isFirstItem={index === 0}
sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', pr: 1 }}
>
<Typography variant="h5" component="h2">
{translateExtensionLanguage(groupName)}
</Typography>
{isUpdateGroup && (
<Button
disabled={!!updatingExtensionIds.length}
variant="contained"
onClick={() => {
const extensionIds = groupExtensions.map((extension) => extension.pkgName);
setUpdatingExtensionIds(extensionIds);
requestManager
.updateExtensions(extensionIds, { update: true })
.response.then(() => handleExtensionUpdate())
.catch(() =>
makeToast(
t(
EXTENSION_ACTION_TO_FAILURE_TRANSLATION_KEY_MAP[
ExtensionAction.UPDATE
],
{ count: groupedExtensions.length },
),
),
)
.finally(() => setUpdatingExtensionIds([]));
}}
>
{t('extension.action.label.update_all')}
</Button>
)}
</StyledGroupHeader>
);
}}
@@ -253,6 +291,9 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
extension={item}
handleUpdate={handleExtensionUpdate}
showSourceRepo={areMultipleReposInUse}
forcedState={
updatingExtensionIds.includes(item.pkgName) ? ExtensionState.UPDATING : undefined
}
/>
</StyledGroupItemWrapper>
);

View File

@@ -14,6 +14,7 @@ import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
@@ -149,8 +150,10 @@ export const LibraryDuplicates = () => {
<StyledGroupedVirtuoso
groupCounts={mangasCountByTitle}
groupContent={(index) => (
<StyledGroupHeader variant="h5" isFirstItem={index === 0}>
<StyledGroupHeader isFirstItem={index === 0}>
<Typography variant="h5" component="h2">
{duplicatedTitles[index]}
</Typography>
</StyledGroupHeader>
)}
computeItemKey={computeItemKey}
@@ -170,8 +173,10 @@ export const LibraryDuplicates = () => {
return duplicatedTitles.map((title, index) => (
<Box key={title}>
<StyledGroupHeader sx={{ pt: index === 0 ? undefined : 0, pb: 0 }} variant="h5" isFirstItem={false}>
<StyledGroupHeader sx={{ pt: index === 0 ? undefined : 0, pb: 0 }} isFirstItem={false}>
<Typography variant="h5" component="h2">
{title}
</Typography>
</StyledGroupHeader>
<BaseMangaGrid
mangas={mangasByTitle[title] as IMangaGridProps['mangas']}

View File

@@ -172,8 +172,10 @@ export const Updates: React.FC = () => {
endReached={loadMore}
groupCounts={groupCounts}
groupContent={(index) => (
<StyledGroupHeader variant="h5" component="h2" isFirstItem={index === 0}>
<StyledGroupHeader sx={{ pt: index === 0 ? undefined : 0, pb: 0 }} isFirstItem={index === 0}>
<Typography variant="h5" component="h2">
{groupedUpdates[index][0]}
</Typography>
</StyledGroupHeader>
)}
computeItemKey={computeItemKey}