2024-03-11 17:02:15 +02:00
|
|
|
import * as React from 'react';
|
2024-04-10 12:39:18 +02:00
|
|
|
import { Icon, Card, Chip, Text } from 'react-native-paper';
|
2024-03-11 17:02:15 +02:00
|
|
|
import { _ } from '@joplin/lib/locale';
|
2024-04-11 09:37:20 +02:00
|
|
|
import { Alert, Linking, StyleSheet, View } from 'react-native';
|
2024-04-08 13:36:40 +02:00
|
|
|
import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
2024-04-03 19:51:09 +02:00
|
|
|
import shim from '@joplin/lib/shim';
|
|
|
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
2024-04-08 13:36:40 +02:00
|
|
|
import ActionButton, { PluginCallback } from './ActionButton';
|
2024-04-25 15:02:10 +02:00
|
|
|
import PluginInfoButton from './PluginInfoButton';
|
2024-03-11 17:02:15 +02:00
|
|
|
|
|
|
|
export enum InstallState {
|
|
|
|
NotInstalled,
|
|
|
|
Installing,
|
|
|
|
Installed,
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum UpdateState {
|
|
|
|
Idle = 1,
|
|
|
|
CanUpdate = 2,
|
|
|
|
Updating = 3,
|
|
|
|
HasBeenUpdated = 4,
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Props {
|
2024-04-25 15:02:10 +02:00
|
|
|
themeId: number;
|
2024-03-11 17:02:15 +02:00
|
|
|
item: PluginItem;
|
|
|
|
isCompatible: boolean;
|
|
|
|
|
|
|
|
hasErrors?: boolean;
|
|
|
|
installState?: InstallState;
|
|
|
|
updateState?: UpdateState;
|
|
|
|
|
2024-04-25 15:02:10 +02:00
|
|
|
onAboutPress?: PluginCallback;
|
2024-03-11 17:02:15 +02:00
|
|
|
onInstall?: PluginCallback;
|
|
|
|
onUpdate?: PluginCallback;
|
|
|
|
onDelete?: PluginCallback;
|
|
|
|
onToggle?: PluginCallback;
|
|
|
|
onShowPluginLog?: PluginCallback;
|
|
|
|
}
|
|
|
|
|
2024-04-08 15:52:07 +02:00
|
|
|
const onRecommendedPress = () => {
|
|
|
|
Alert.alert(
|
|
|
|
'',
|
|
|
|
_('The Joplin team has vetted this plugin and it meets our standards for security and performance.'),
|
|
|
|
[
|
|
|
|
{
|
|
|
|
text: _('Learn more'),
|
|
|
|
onPress: () => Linking.openURL('https://github.com/joplin/plugins/blob/master/readme/recommended.md'),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
text: _('OK'),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
{ cancelable: true },
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-04-05 13:16:49 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2024-03-11 17:02:15 +02:00
|
|
|
const PluginIcon = (props: any) => <Icon {...props} source='puzzle'/>;
|
|
|
|
|
2024-04-11 09:37:20 +02:00
|
|
|
const styles = StyleSheet.create({
|
|
|
|
versionText: {
|
|
|
|
opacity: 0.8,
|
|
|
|
},
|
|
|
|
title: {
|
|
|
|
// Prevents the title text from being clipped on Android
|
|
|
|
verticalAlign: 'middle',
|
|
|
|
},
|
|
|
|
});
|
2024-04-10 12:39:18 +02:00
|
|
|
|
2024-03-11 17:02:15 +02:00
|
|
|
const PluginBox: React.FC<Props> = props => {
|
|
|
|
const manifest = props.item.manifest;
|
|
|
|
const item = props.item;
|
|
|
|
|
|
|
|
const installButtonTitle = () => {
|
|
|
|
if (props.installState === InstallState.Installing) return _('Installing...');
|
|
|
|
if (props.installState === InstallState.NotInstalled) return _('Install');
|
|
|
|
if (props.installState === InstallState.Installed) return _('Installed');
|
|
|
|
return `Invalid install state: ${props.installState}`;
|
|
|
|
};
|
|
|
|
|
|
|
|
const installButton = (
|
2024-04-08 13:36:40 +02:00
|
|
|
<ActionButton
|
|
|
|
item={item}
|
|
|
|
onPress={props.onInstall}
|
2024-03-29 14:40:54 +02:00
|
|
|
disabled={props.installState !== InstallState.NotInstalled || !props.isCompatible}
|
2024-03-11 17:02:15 +02:00
|
|
|
loading={props.installState === InstallState.Installing}
|
2024-04-08 13:36:40 +02:00
|
|
|
title={installButtonTitle()}
|
|
|
|
/>
|
2024-03-11 17:02:15 +02:00
|
|
|
);
|
|
|
|
|
2024-04-08 13:36:40 +02:00
|
|
|
const getUpdateButtonTitle = () => {
|
2024-03-11 17:02:15 +02:00
|
|
|
if (props.updateState === UpdateState.Updating) return _('Updating...');
|
|
|
|
if (props.updateState === UpdateState.HasBeenUpdated) return _('Updated');
|
|
|
|
return _('Update');
|
|
|
|
};
|
|
|
|
|
|
|
|
const updateButton = (
|
2024-04-08 13:36:40 +02:00
|
|
|
<ActionButton
|
|
|
|
item={item}
|
|
|
|
onPress={props.onUpdate}
|
2024-03-29 14:40:54 +02:00
|
|
|
disabled={props.updateState !== UpdateState.CanUpdate || !props.isCompatible}
|
2024-03-11 17:02:15 +02:00
|
|
|
loading={props.updateState === UpdateState.Updating}
|
2024-04-08 13:36:40 +02:00
|
|
|
title={getUpdateButtonTitle()}
|
|
|
|
/>
|
2024-03-11 17:02:15 +02:00
|
|
|
);
|
2024-04-08 13:36:40 +02:00
|
|
|
|
2024-03-11 17:02:15 +02:00
|
|
|
const deleteButton = (
|
2024-04-08 13:36:40 +02:00
|
|
|
<ActionButton
|
|
|
|
item={item}
|
|
|
|
onPress={props.onDelete}
|
2024-03-11 17:02:15 +02:00
|
|
|
disabled={props.item.deleted}
|
2024-04-08 13:36:40 +02:00
|
|
|
title={props.item.deleted ? _('Deleted') : _('Delete')}
|
|
|
|
/>
|
2024-03-11 17:02:15 +02:00
|
|
|
);
|
2024-04-08 13:36:40 +02:00
|
|
|
const disableButton = <ActionButton item={item} onPress={props.onToggle} title={_('Disable')}/>;
|
|
|
|
const enableButton = <ActionButton item={item} onPress={props.onToggle} title={_('Enable')}/>;
|
2024-04-25 15:02:10 +02:00
|
|
|
const aboutButton = <ActionButton item={item} onPress={props.onAboutPress} icon='web' title={_('About')}/>;
|
2024-03-11 17:02:15 +02:00
|
|
|
|
|
|
|
const renderErrorsChip = () => {
|
|
|
|
if (!props.hasErrors) return null;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Chip
|
|
|
|
icon='alert'
|
|
|
|
mode='outlined'
|
|
|
|
onPress={() => props.onShowPluginLog({ item })}
|
|
|
|
>
|
2024-04-08 13:36:40 +02:00
|
|
|
{_('Error')}
|
2024-03-11 17:02:15 +02:00
|
|
|
</Chip>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const renderRecommendedChip = () => {
|
2024-04-03 19:51:09 +02:00
|
|
|
if (!props.item.manifest._recommended || !props.isCompatible) {
|
2024-03-11 17:02:15 +02:00
|
|
|
return null;
|
|
|
|
}
|
2024-04-08 15:52:07 +02:00
|
|
|
return <Chip
|
|
|
|
icon='crown'
|
|
|
|
mode='outlined'
|
|
|
|
onPress={onRecommendedPress}
|
|
|
|
>
|
|
|
|
{_('Recommended')}
|
|
|
|
</Chip>;
|
2024-03-11 17:02:15 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const renderBuiltInChip = () => {
|
|
|
|
if (!props.item.builtIn) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return <Chip icon='code-tags-check' mode='outlined'>{_('Built-in')}</Chip>;
|
|
|
|
};
|
|
|
|
|
2024-04-03 19:51:09 +02:00
|
|
|
const renderIncompatibleChip = () => {
|
|
|
|
if (props.isCompatible) return null;
|
|
|
|
return (
|
|
|
|
<Chip
|
|
|
|
icon='alert'
|
|
|
|
mode='outlined'
|
|
|
|
onPress={() => {
|
|
|
|
void shim.showMessageBox(
|
|
|
|
PluginService.instance().describeIncompatibility(props.item.manifest),
|
|
|
|
{ buttons: [_('OK')] },
|
|
|
|
);
|
|
|
|
}}
|
|
|
|
>{_('Incompatible')}</Chip>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-04-25 15:02:10 +02:00
|
|
|
const renderRightEdgeButton = (buttonProps: { size: number }) => {
|
|
|
|
// If .onAboutPress is given (e.g. when searching), there's another way to get information
|
|
|
|
// about the plugin. In this case, we don't show the right-side information link.
|
|
|
|
if (props.onAboutPress) return null;
|
|
|
|
return <PluginInfoButton {...buttonProps} themeId={props.themeId} item={props.item}/>;
|
|
|
|
};
|
|
|
|
|
2024-03-11 17:02:15 +02:00
|
|
|
const updateStateIsIdle = props.updateState !== UpdateState.Idle;
|
|
|
|
|
2024-04-10 12:39:18 +02:00
|
|
|
const titleComponent = <>
|
2024-04-11 09:37:20 +02:00
|
|
|
<Text variant='titleMedium'>{manifest.name}</Text> <Text variant='bodySmall' style={styles.versionText}>v{manifest.version}</Text>
|
2024-04-10 12:39:18 +02:00
|
|
|
</>;
|
2024-03-11 17:02:15 +02:00
|
|
|
return (
|
2024-04-03 19:51:09 +02:00
|
|
|
<Card style={{ margin: 8, opacity: props.isCompatible ? undefined : 0.75 }} testID='plugin-card'>
|
2024-03-11 17:02:15 +02:00
|
|
|
<Card.Title
|
2024-04-10 12:39:18 +02:00
|
|
|
title={titleComponent}
|
2024-04-11 09:37:20 +02:00
|
|
|
titleStyle={styles.title}
|
2024-03-11 17:02:15 +02:00
|
|
|
subtitle={manifest.description}
|
|
|
|
left={PluginIcon}
|
2024-04-25 15:02:10 +02:00
|
|
|
right={renderRightEdgeButton}
|
2024-03-11 17:02:15 +02:00
|
|
|
/>
|
|
|
|
<Card.Content>
|
|
|
|
<View style={{ flexDirection: 'row' }}>
|
2024-04-03 19:51:09 +02:00
|
|
|
{renderIncompatibleChip()}
|
2024-03-11 17:02:15 +02:00
|
|
|
{renderErrorsChip()}
|
|
|
|
{renderRecommendedChip()}
|
|
|
|
{renderBuiltInChip()}
|
|
|
|
</View>
|
|
|
|
</Card.Content>
|
|
|
|
<Card.Actions>
|
|
|
|
{props.onAboutPress ? aboutButton : null}
|
|
|
|
{props.onInstall ? installButton : null}
|
|
|
|
{props.onDelete && !props.item.builtIn ? deleteButton : null}
|
|
|
|
{props.onUpdate && updateStateIsIdle ? updateButton : null}
|
|
|
|
{props.onToggle && props.item.enabled ? disableButton : null}
|
|
|
|
{props.onToggle && !props.item.enabled ? enableButton : null}
|
|
|
|
</Card.Actions>
|
|
|
|
</Card>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default PluginBox;
|