1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-27 10:32:58 +02:00
joplin/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx

266 lines
7.3 KiB
TypeScript

import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import { useCallback, useMemo } from 'react';
import { Card, Divider, List, Portal, Switch, Text } from 'react-native-paper';
import getPluginIssueReportUrl from '@joplin/lib/services/plugins/utils/getPluginIssueReportUrl';
import { Linking, ScrollView, StyleSheet, View, ViewStyle } from 'react-native';
import DismissibleDialog, { DialogSize } from '../../../DismissibleDialog';
import openWebsiteForPlugin from './utils/openWebsiteForPlugin';
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import PluginTitle from './PluginBox/PluginTitle';
import ActionButton from './buttons/ActionButton';
import TextButton, { ButtonType } from '../../../buttons/TextButton';
import useUpdateState, { UpdateState } from './utils/useUpdateState';
import { PluginCallback, PluginCallbacks } from './utils/usePluginCallbacks';
import usePluginItem from './utils/usePluginItem';
import InstallButton from './buttons/InstallButton';
import { InstallState } from './PluginBox';
import PluginChips from './PluginBox/PluginChips';
import { PluginStatusRecord } from '../types';
interface Props {
themeId: number;
visible: boolean;
item: PluginItem|null;
updatablePluginIds: PluginStatusRecord;
updatingPluginIds: PluginStatusRecord;
installingPluginIds: PluginStatusRecord;
pluginCallbacks: PluginCallbacks;
pluginSettings: PluginSettings;
onModalDismiss: ()=> void;
}
const styles = (() => {
const baseButtonContainer: ViewStyle = {
display: 'flex',
flexDirection: 'column',
gap: 20,
marginLeft: 10,
marginRight: 10,
};
return StyleSheet.create({
descriptionText: {
marginTop: 5,
marginBottom: 5,
},
buttonContainer: {
...baseButtonContainer,
marginTop: 26,
marginBottom: 26,
},
accordionContent: {
...baseButtonContainer,
marginTop: 12,
},
fraudulentPluginButton: {
opacity: 0.6,
},
enabledSwitchContainer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 10,
marginTop: 12,
marginBottom: 14,
},
pluginDescriptionContainer: {
marginTop: 8,
gap: 8,
},
});
})();
interface EnabledSwitchProps {
item: PluginItem;
onToggle: PluginCallback;
}
const EnabledSwitch: React.FC<EnabledSwitchProps> = props => {
const onChange = useCallback((value: boolean) => {
if (value !== props.item.enabled) {
props.onToggle({ item: props.item });
}
}, [props.item, props.onToggle]);
if (!props.item?.installed || props.item.deleted) {
return null;
}
return <View style={styles.enabledSwitchContainer}>
<Text nativeID='enabledLabel'>{_('Enabled')}</Text>
<Switch accessibilityLabelledBy='enabledLabel' value={props.item.enabled} onValueChange={onChange} />
</View>;
};
const PluginInfoModalContent: React.FC<Props> = props => {
const initialItem = props.item;
const pluginId = initialItem.manifest.id;
const item = usePluginItem(pluginId, props.pluginSettings, initialItem);
const manifest = item.manifest;
const isCompatible = useMemo(() => {
return PluginService.instance().isCompatible(manifest);
}, [manifest]);
const plugin = useMemo(() => {
const service = PluginService.instance();
if (!service.pluginIds.includes(pluginId)) {
return null;
}
return service.pluginById(pluginId);
}, [pluginId]);
const updateState = useUpdateState({
pluginId: plugin?.id,
pluginSettings: props.pluginSettings,
updatablePluginIds: props.updatablePluginIds,
updatingPluginIds: props.updatingPluginIds,
});
const aboutPlugin = (
<Card mode='outlined' style={{ margin: 8 }} testID='plugin-card'>
<Card.Content>
<PluginTitle manifest={manifest}/>
<Text variant='bodyMedium'>{_('by %s', manifest.author)}</Text>
<View style={styles.pluginDescriptionContainer}>
<PluginChips
themeId={props.themeId}
item={item}
showInstalledChip={false}
hasErrors={plugin.hasErrors}
canUpdate={false}
onShowPluginLog={props.pluginCallbacks.onShowPluginLog}
isCompatible={isCompatible}
/>
<Text>{manifest.description}</Text>
</View>
</Card.Content>
</Card>
);
const onAboutPress = useCallback(() => {
void openWebsiteForPlugin({ item });
}, [item]);
const reportIssueUrl = useMemo(() => {
return getPluginIssueReportUrl(manifest);
}, [manifest]);
const onReportIssuePress = useCallback(() => {
void Linking.openURL(reportIssueUrl);
}, [reportIssueUrl]);
const reportIssueButton = (
<TextButton
type={ButtonType.Secondary}
onPress={onReportIssuePress}
>{_('Report an issue')}</TextButton>
);
const onReportFraudulentPress = useCallback(() => {
void Linking.openURL('https://github.com/laurent22/joplin/security/advisories/new');
}, []);
const getUpdateButtonTitle = () => {
if (updateState === UpdateState.Updating) return _('Updating...');
if (updateState === UpdateState.HasBeenUpdated) return _('Updated');
return _('Update');
};
const updateButton = (
<ActionButton
item={item}
type={ButtonType.Secondary}
onPress={props.pluginCallbacks.onUpdate}
disabled={updateState !== UpdateState.CanUpdate || !isCompatible}
loading={updateState === UpdateState.Updating}
title={getUpdateButtonTitle()}
/>
);
const installState = (() => {
if (item.installed) return InstallState.Installed;
if (props.installingPluginIds[pluginId]) return InstallState.Installing;
return InstallState.NotInstalled;
})();
const installButton = (
<InstallButton
item={item}
onInstall={props.pluginCallbacks.onInstall}
installState={installState}
isCompatible={isCompatible}
/>
);
const deleteButton = (
<ActionButton
item={item}
type={ButtonType.Delete}
onPress={props.pluginCallbacks.onDelete}
disabled={item.builtIn || (item?.deleted ?? true)}
title={item?.deleted ? _('Deleted') : _('Delete')}
/>
);
const deleteButtonContainer = <>
<View style={styles.buttonContainer}>
{deleteButton}
</View>
<Divider />
</>;
const reportIssuesContainer = (
<List.Accordion title={_('Report any issues concerning the plugin.')} titleNumberOfLines={2}>
<View style={styles.accordionContent}>
<TextButton
type={ButtonType.Secondary}
onPress={onReportFraudulentPress}
>{_('Report fraudulent plugin')}</TextButton>
{reportIssueButton}
</View>
</List.Accordion>
);
return <>
<ScrollView>
{aboutPlugin}
<EnabledSwitch item={item} onToggle={props.pluginCallbacks.onToggle}/>
<Divider />
<View style={styles.buttonContainer}>
{!item.installed ? installButton : null}
<TextButton
type={item.installed ? ButtonType.Primary : ButtonType.Secondary}
onPress={onAboutPress}
>{_('About')}</TextButton>
{updateState !== UpdateState.Idle ? updateButton : null}
</View>
<Divider />
{ item.installed ? deleteButtonContainer : null }
{reportIssuesContainer}
</ScrollView>
</>;
};
const PluginInfoModal: React.FC<Props> = props => {
return (
<Portal>
<DismissibleDialog
themeId={props.themeId}
visible={props.visible}
size={DialogSize.Small}
onDismiss={props.onModalDismiss}
>
{ props.item ? <PluginInfoModalContent {...props}/> : null }
</DismissibleDialog>
</Portal>
);
};
export default PluginInfoModal;