2021-01-07 18:30:53 +02:00
|
|
|
import * as React from 'react';
|
|
|
|
import { useCallback, useMemo, useState } from 'react';
|
|
|
|
import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
|
|
|
import { _ } from '@joplin/lib/locale';
|
|
|
|
import styled from 'styled-components';
|
|
|
|
import SearchPlugins from './SearchPlugins';
|
|
|
|
import PluginBox from './PluginBox';
|
2021-01-09 15:14:39 +02:00
|
|
|
import Button, { ButtonLevel } from '../../../Button/Button';
|
2021-01-07 18:30:53 +02:00
|
|
|
import bridge from '../../../../services/bridge';
|
|
|
|
import produce from 'immer';
|
|
|
|
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
|
|
|
|
import { PluginItem } from './PluginBox';
|
|
|
|
const { space } = require('styled-system');
|
|
|
|
|
2021-01-09 15:14:39 +02:00
|
|
|
const maxWidth: number = 250;
|
|
|
|
|
2021-01-07 18:30:53 +02:00
|
|
|
const Root = styled.div`
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
`;
|
|
|
|
|
|
|
|
const UserPluginsRoot = styled.div`
|
|
|
|
${space}
|
|
|
|
display: flex;
|
|
|
|
flex-wrap: wrap;
|
|
|
|
`;
|
|
|
|
|
2021-01-09 15:14:39 +02:00
|
|
|
const ToolsButton = styled(Button)`
|
|
|
|
margin-right: 2px;
|
|
|
|
`;
|
2021-01-07 18:30:53 +02:00
|
|
|
|
|
|
|
interface Props {
|
|
|
|
value: any;
|
|
|
|
themeId: number;
|
|
|
|
onChange: Function;
|
|
|
|
renderLabel: Function;
|
|
|
|
renderDescription: Function;
|
2021-01-09 15:14:39 +02:00
|
|
|
renderHeader: Function;
|
2021-01-07 18:30:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[] {
|
|
|
|
return useMemo(() => {
|
|
|
|
const output: PluginItem[] = [];
|
|
|
|
|
|
|
|
for (const pluginId in plugins) {
|
|
|
|
const plugin = plugins[pluginId];
|
|
|
|
|
|
|
|
const setting: PluginSetting = {
|
|
|
|
...defaultPluginSetting(),
|
|
|
|
...settings[pluginId],
|
|
|
|
};
|
|
|
|
|
|
|
|
output.push({
|
|
|
|
id: pluginId,
|
|
|
|
name: plugin.manifest.name,
|
|
|
|
description: plugin.manifest.description,
|
|
|
|
enabled: setting.enabled,
|
|
|
|
deleted: setting.deleted,
|
|
|
|
devMode: plugin.devMode,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
output.sort((a: PluginItem, b: PluginItem) => {
|
|
|
|
return a.name < b.name ? -1 : +1;
|
|
|
|
});
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}, [plugins, settings]);
|
|
|
|
}
|
|
|
|
|
|
|
|
export default function(props: Props) {
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
|
|
|
const pluginService = PluginService.instance();
|
|
|
|
|
|
|
|
const pluginSettings = useMemo(() => {
|
|
|
|
return pluginService.unserializePluginSettings(props.value);
|
|
|
|
}, [props.value]);
|
|
|
|
|
|
|
|
const onDelete = useCallback(async (event: any) => {
|
|
|
|
const item: PluginItem = event.item;
|
|
|
|
const confirm = await bridge().showConfirmMessageBox(_('Delete plugin "%s"?', item.name));
|
|
|
|
if (!confirm) return;
|
|
|
|
|
|
|
|
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
|
|
|
if (!draft[item.id]) draft[item.id] = defaultPluginSetting();
|
|
|
|
draft[item.id].deleted = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
|
|
|
}, [pluginSettings, props.onChange]);
|
|
|
|
|
|
|
|
const onToggle = useCallback((event: any) => {
|
|
|
|
const item: PluginItem = event.item;
|
|
|
|
|
|
|
|
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
|
|
|
if (!draft[item.id]) draft[item.id] = defaultPluginSetting();
|
|
|
|
draft[item.id].enabled = !draft[item.id].enabled;
|
|
|
|
});
|
|
|
|
|
|
|
|
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
|
|
|
}, [pluginSettings, props.onChange]);
|
|
|
|
|
2021-01-09 15:14:39 +02:00
|
|
|
const onInstall = useCallback(async () => {
|
|
|
|
const result = bridge().showOpenDialog({
|
|
|
|
filters: [{ name: 'Joplin Plugin Archive', extensions: ['jpl'] }],
|
|
|
|
});
|
2021-01-07 18:30:53 +02:00
|
|
|
|
2021-01-09 15:14:39 +02:00
|
|
|
const filePath = result && result.length ? result[0] : null;
|
|
|
|
if (!filePath) return;
|
2021-01-07 18:30:53 +02:00
|
|
|
|
2021-01-09 15:14:39 +02:00
|
|
|
const plugin = await pluginService.installPlugin(filePath);
|
2021-01-07 18:30:53 +02:00
|
|
|
|
2021-01-09 15:14:39 +02:00
|
|
|
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
|
|
|
draft[plugin.manifest.id] = defaultPluginSetting();
|
|
|
|
});
|
2021-01-07 18:30:53 +02:00
|
|
|
|
2021-01-09 15:14:39 +02:00
|
|
|
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
|
|
|
}, [pluginSettings, props.onChange]);
|
|
|
|
|
|
|
|
const onToolsClick = useCallback(async () => {
|
|
|
|
const template = [];
|
|
|
|
|
|
|
|
template.push({
|
|
|
|
label: _('Install from file'),
|
|
|
|
click: onInstall,
|
|
|
|
});
|
|
|
|
|
|
|
|
const menu = bridge().Menu.buildFromTemplate(template);
|
|
|
|
menu.popup(bridge().window());
|
|
|
|
}, [onInstall]);
|
2021-01-07 18:30:53 +02:00
|
|
|
|
|
|
|
const onSearchQueryChange = useCallback((event: OnChangeEvent) => {
|
|
|
|
setSearchQuery(event.value);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const onSearchPluginSettingsChange = useCallback((event: any) => {
|
|
|
|
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
|
|
|
|
}, [props.onChange]);
|
|
|
|
|
|
|
|
function renderCells(items: PluginItem[]) {
|
|
|
|
const output = [];
|
|
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
if (item.deleted) continue;
|
|
|
|
|
|
|
|
output.push(<PluginBox
|
|
|
|
key={item.id}
|
|
|
|
item={item}
|
|
|
|
themeId={props.themeId}
|
|
|
|
onDelete={onDelete}
|
|
|
|
onToggle={onToggle}
|
|
|
|
/>);
|
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderUserPlugins(pluginItems: PluginItem[]) {
|
|
|
|
const allDeleted = !pluginItems.find(it => it.deleted !== true);
|
|
|
|
|
|
|
|
if (!pluginItems.length || allDeleted) {
|
|
|
|
return (
|
|
|
|
<UserPluginsRoot mb={'10px'}>
|
|
|
|
{props.renderDescription(props.themeId, _('You do not have any installed plugin.'))}
|
|
|
|
</UserPluginsRoot>
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return (
|
|
|
|
<UserPluginsRoot>
|
|
|
|
{renderCells(pluginItems)}
|
|
|
|
</UserPluginsRoot>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Root>
|
|
|
|
<div style={{ marginBottom: 20 }}>
|
2021-01-09 15:14:39 +02:00
|
|
|
{props.renderHeader(props.themeId, _('Search for plugins'))}
|
2021-01-07 18:30:53 +02:00
|
|
|
<SearchPlugins
|
2021-01-09 15:14:39 +02:00
|
|
|
maxWidth={maxWidth}
|
2021-01-07 18:30:53 +02:00
|
|
|
themeId={props.themeId}
|
|
|
|
searchQuery={searchQuery}
|
|
|
|
pluginSettings={pluginSettings}
|
|
|
|
onSearchQueryChange={onSearchQueryChange}
|
|
|
|
onPluginSettingsChange={onSearchPluginSettingsChange}
|
|
|
|
renderDescription={props.renderDescription}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
|
2021-01-09 15:14:39 +02:00
|
|
|
<div>
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'row', maxWidth }}>
|
|
|
|
<div style={{ display: 'flex', flex: 1 }}>
|
|
|
|
{props.renderHeader(props.themeId, _('Manage your plugins'))}
|
|
|
|
</div>
|
|
|
|
<ToolsButton tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Primary} onClick={onToolsClick}/>
|
|
|
|
</div>
|
|
|
|
{renderUserPlugins(pluginItems)}
|
|
|
|
</div>
|
2021-01-07 18:30:53 +02:00
|
|
|
</Root>
|
|
|
|
);
|
|
|
|
}
|