1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-24 08:12:24 +02:00

Desktop: Add config screen to add, remove or enable, disable plugins

This commit is contained in:
Laurent Cozic 2020-11-19 12:34:49 +00:00
parent f36019c94d
commit e57444dc32
24 changed files with 736 additions and 138 deletions

View File

@ -241,6 +241,15 @@ packages/app-cli/tests/support/plugins/events/api/types.js.map
packages/app-cli/tests/support/plugins/events/src/index.d.ts
packages/app-cli/tests/support/plugins/events/src/index.js
packages/app-cli/tests/support/plugins/events/src/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/index.js
packages/app-cli/tests/support/plugins/jpl_test/api/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/types.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/types.js
packages/app-cli/tests/support/plugins/jpl_test/api/types.js.map
packages/app-cli/tests/support/plugins/jpl_test/src/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/src/index.js
packages/app-cli/tests/support/plugins/jpl_test/src/index.js.map
packages/app-cli/tests/support/plugins/json_export/api/index.d.ts
packages/app-cli/tests/support/plugins/json_export/api/index.js
packages/app-cli/tests/support/plugins/json_export/api/index.js.map
@ -367,6 +376,9 @@ packages/app-desktop/gui/ConfigScreen/ConfigScreen.js.map
packages/app-desktop/gui/ConfigScreen/SideBar.d.ts
packages/app-desktop/gui/ConfigScreen/SideBar.js
packages/app-desktop/gui/ConfigScreen/SideBar.js.map
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.d.ts
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
@ -730,6 +742,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js.map
packages/app-desktop/gui/ToolbarButton/styles/index.d.ts
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarButton/styles/index.js.map
packages/app-desktop/gui/dialogs.d.ts
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/dialogs.js.map
packages/app-desktop/gui/hooks/useEffectDebugger.d.ts
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useEffectDebugger.js.map
@ -742,6 +757,9 @@ packages/app-desktop/gui/hooks/usePrevious.js.map
packages/app-desktop/gui/hooks/usePropsDebugger.d.ts
packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/hooks/usePropsDebugger.js.map
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
packages/app-desktop/gui/menuCommandNames.d.ts
packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map

18
.gitignore vendored
View File

@ -233,6 +233,15 @@ packages/app-cli/tests/support/plugins/events/api/types.js.map
packages/app-cli/tests/support/plugins/events/src/index.d.ts
packages/app-cli/tests/support/plugins/events/src/index.js
packages/app-cli/tests/support/plugins/events/src/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/index.js
packages/app-cli/tests/support/plugins/jpl_test/api/index.js.map
packages/app-cli/tests/support/plugins/jpl_test/api/types.d.ts
packages/app-cli/tests/support/plugins/jpl_test/api/types.js
packages/app-cli/tests/support/plugins/jpl_test/api/types.js.map
packages/app-cli/tests/support/plugins/jpl_test/src/index.d.ts
packages/app-cli/tests/support/plugins/jpl_test/src/index.js
packages/app-cli/tests/support/plugins/jpl_test/src/index.js.map
packages/app-cli/tests/support/plugins/json_export/api/index.d.ts
packages/app-cli/tests/support/plugins/json_export/api/index.js
packages/app-cli/tests/support/plugins/json_export/api/index.js.map
@ -359,6 +368,9 @@ packages/app-desktop/gui/ConfigScreen/ConfigScreen.js.map
packages/app-desktop/gui/ConfigScreen/SideBar.d.ts
packages/app-desktop/gui/ConfigScreen/SideBar.js
packages/app-desktop/gui/ConfigScreen/SideBar.js.map
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.d.ts
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map
packages/app-desktop/gui/DropboxLoginScreen.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map
@ -722,6 +734,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js.map
packages/app-desktop/gui/ToolbarButton/styles/index.d.ts
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarButton/styles/index.js.map
packages/app-desktop/gui/dialogs.d.ts
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/dialogs.js.map
packages/app-desktop/gui/hooks/useEffectDebugger.d.ts
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useEffectDebugger.js.map
@ -734,6 +749,9 @@ packages/app-desktop/gui/hooks/usePrevious.js.map
packages/app-desktop/gui/hooks/usePropsDebugger.d.ts
packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/hooks/usePropsDebugger.js.map
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
packages/app-desktop/gui/menuCommandNames.d.ts
packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map

View File

@ -13,6 +13,7 @@
"_vieux/": true,
".gitignore": true,
".eslintignore": true,
"**/*.jpl": true,
"./packages/app-cli/**/*.*~": true,
"./packages/app-cli/**/*.mo": true,
"./packages/app-cli/**/build/": true,
@ -55,7 +56,6 @@
"./packages/app-desktop/**/*.min.js": true,
"./packages/app-desktop/**/dist/": true,
"./packages/app-desktop/**/gui/note-viewer/pluginAssets/": true,
"./packages/app-desktop/**/lib/": true,
"./packages/app-desktop/**/node_modules/": true,
"./packages/app-desktop/**/packageInfo.js": true,
"./packages/app-desktop/**/pluginAssets/": true,
@ -242,7 +242,6 @@
"packages/app-desktop/**/*.min.js": true,
"packages/app-desktop/**/dist/": true,
"packages/app-desktop/**/gui/note-viewer/pluginAssets/": true,
"packages/app-desktop/**/lib/": true,
"packages/app-desktop/**/node_modules/": true,
"packages/app-desktop/**/packageInfo.js": true,
"packages/app-desktop/**/pluginAssets/": true,

View File

@ -4,7 +4,7 @@ import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
import MdToHtml from '@joplin/renderer/MdToHtml';
import shim from '@joplin/lib/shim';
const { asyncTest, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js');
const { asyncTest, expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir } = require('./test-utils.js');
const Note = require('@joplin/lib/models/Note');
const Folder = require('@joplin/lib/models/Folder');
@ -43,7 +43,7 @@ describe('services_PluginService', function() {
it('should load and run a simple plugin', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple`]);
await service.loadAndRunPlugins([`${testPluginDir}/simple`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
@ -59,13 +59,13 @@ describe('services_PluginService', function() {
it('should load and run a simple plugin and handle trailing slash', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple/`]);
await service.loadAndRunPlugins([`${testPluginDir}/simple/`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
}));
it('should load and run a plugin that uses external packages', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`]);
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`], {});
expect(() => service.pluginById('org.joplinapp.plugins.ExternalModuleDemo')).not.toThrowError();
const allFolders = await Folder.all();
@ -78,7 +78,7 @@ describe('services_PluginService', function() {
it('should load multiple plugins from a directory', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`);
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`, {});
const plugin1 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo1');
const plugin2 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo2');
@ -125,14 +125,14 @@ describe('services_PluginService', function() {
it('should load plugins from JS bundle files', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`);
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`, {});
expect(!!service.pluginById('org.joplinapp.plugins.JsBundleDemo')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
it('should load plugins from JPL archive', asyncTest(async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`]);
await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`], {});
expect(!!service.pluginById('org.joplinapp.FirstJplPlugin')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
@ -248,9 +248,15 @@ describe('services_PluginService', function() {
];
for (const testCase of testCases) {
const [appVersion, expected] = testCase;
const plugin = await newPluginService(appVersion as string).loadPluginFromJsBundle('', pluginScript);
expect(plugin.enabled).toBe(expected as boolean);
const [appVersion, hasNoError] = testCase;
const service = newPluginService(appVersion as string);
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
if (hasNoError) {
await expectNotThrow(() => service.runPlugin(plugin));
} else {
await expectThrow(() => service.runPlugin(plugin));
}
}
}));

View File

@ -1,7 +1,6 @@
node_modules/
packageInfo.js
dist/
lib/
*.min.js
.DS_Store
gui/note-viewer/pluginAssets/

View File

@ -495,12 +495,25 @@ class Application extends BaseApplication {
pluginLogger.addTarget(TargetType.Console, { prefix: 'Plugin Service:' });
pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO);
const service = PluginService.instance();
const pluginRunner = new PluginRunner();
PluginService.instance().setLogger(pluginLogger);
PluginService.instance().initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
service.setLogger(pluginLogger);
service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
// Users can add and remove plugins from the config screen at any
// time, however we only effectively uninstall the plugin the next
// time the app is started. What plugin should be uninstalled is
// stored in the settings.
const newSettings = await service.uninstallPlugins(pluginSettings);
Setting.setValue('plugins.states', newSettings);
try {
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir'));
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
}
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error);
}
@ -508,12 +521,12 @@ class Application extends BaseApplication {
try {
if (Setting.value('plugins.devPluginPaths')) {
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim());
await PluginService.instance().loadAndRunPlugins(paths);
await service.loadAndRunPlugins(paths, pluginSettings, true);
}
// Also load dev plugins that have passed via command line arguments
if (Setting.value('startupDevPlugins')) {
await PluginService.instance().loadAndRunPlugins(Setting.value('startupDevPlugins'));
await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, true);
}
} catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, error);
@ -723,6 +736,14 @@ class Application extends BaseApplication {
// console.info(CommandService.instance().commandsToMarkdownTable(this.store().getState()));
// }, 2000);
// this.dispatch({
// type: 'NAV_GO',
// routeName: 'Config',
// props: {
// defaultSection: 'plugins',
// },
// });
return null;
}

View File

@ -87,7 +87,7 @@ export class Bridge {
return filePath;
}
showOpenDialog(options: any) {
showOpenDialog(options: any = null) {
const { dialog } = require('electron');
if (!options) options = {};
let fileType = 'file';
@ -117,13 +117,16 @@ export class Bridge {
}
showConfirmMessageBox(message: string, options: any = null) {
if (options === null) options = {};
options = {
buttons: [_('OK'), _('Cancel')],
...options,
};
const result = this.showMessageBox_(this.window(), Object.assign({}, {
type: 'question',
message: message,
cancelId: 1,
buttons: [_('OK'), _('Cancel')],
buttons: options.buttons,
}, options));
return result === 0;

View File

@ -3,17 +3,23 @@ import SideBar from './SideBar';
import ButtonBar from './ButtonBar';
import Button, { ButtonLevel } from '../Button/Button';
import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge';
import Setting from '@joplin/lib/models/Setting';
import control_PluginsStates from './controls/PluginsStates';
const { connect } = require('react-redux');
const Setting = require('@joplin/lib/models/Setting').default;
const { themeStyle } = require('@joplin/lib/theme');
const pathUtils = require('@joplin/lib/path-utils');
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry');
const shared = require('@joplin/lib/components/shared/config-shared.js');
const bridge = require('electron').remote.require('./bridge').default;
const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min');
const { ClipperConfigScreen } = require('../ClipperConfigScreen.min');
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
const settingKeyToControl: any = {
'plugins.states': control_PluginsStates,
};
class ConfigScreenComponent extends React.Component<any, any> {
rowStyle_: any = null;
@ -27,6 +33,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
selectedSectionName: 'general',
screenName: '',
changedSettingKeys: [],
needRestart: false,
};
this.rowStyle_ = {
@ -41,6 +48,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.onCancelClick = this.onCancelClick.bind(this);
this.onSaveClick = this.onSaveClick.bind(this);
this.onApplyClick = this.onApplyClick.bind(this);
this.renderLabel = this.renderLabel.bind(this);
this.renderDescription = this.renderDescription.bind(this);
}
async checkSyncConfig_() {
@ -261,6 +270,40 @@ class ConfigScreenComponent extends React.Component<any, any> {
);
}
private labelStyle(themeId: number) {
const theme = themeStyle(themeId);
return Object.assign({}, theme.textStyle, {
display: 'block',
color: theme.color,
fontSize: theme.fontSize * 1.083333,
fontWeight: 500,
marginBottom: theme.mainPadding / 4,
});
}
private descriptionStyle(themeId: number) {
const theme = themeStyle(themeId);
return Object.assign({}, theme.textStyle, {
color: theme.colorFaded,
fontStyle: 'italic',
maxWidth: '70em',
marginTop: 5,
});
}
private renderLabel(themeId: number, label: string) {
const labelStyle = this.labelStyle(themeId);
return (
<div style={labelStyle}>
<label>{label}</label>
</div>
);
}
private renderDescription(themeId: number, description: string) {
return description ? <div style={this.descriptionStyle(themeId)}>{description}</div> : null;
}
settingToComponent(key: string, value: any) {
const theme = themeStyle(this.props.themeId);
@ -270,13 +313,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
marginBottom: theme.mainPadding,
};
const labelStyle = Object.assign({}, theme.textStyle, {
display: 'block',
color: theme.color,
fontSize: theme.fontSize * 1.083333,
fontWeight: 500,
marginBottom: theme.mainPadding / 4,
});
const labelStyle = this.labelStyle(this.props.themeId);
const subLabel = Object.assign({}, labelStyle, {
display: 'block',
@ -297,13 +334,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
backgroundColor: theme.backgroundColor,
};
const descriptionStyle = Object.assign({}, theme.textStyle, {
color: theme.colorFaded,
marginTop: 5,
fontStyle: 'italic',
maxWidth: '70em',
});
const textInputBaseStyle = Object.assign({}, controlStyle, {
fontFamily: theme.fontFamily,
border: '1px solid',
@ -318,18 +348,39 @@ class ConfigScreenComponent extends React.Component<any, any> {
});
const updateSettingValue = (key: string, value: any) => {
// console.info(key + ' = ' + value);
return shared.updateSettingValue(this, key, value);
};
const md = Setting.settingMetadata(key);
if (md.needRestart) {
this.setState({ needRestart: true });
}
shared.updateSettingValue(this, key, value);
// Component key needs to be key+value otherwise it doesn't update when the settings change.
if (md.autoSave) {
shared.scheduleSaveSettings(this);
}
};
const md = Setting.settingMetadata(key);
const descriptionText = Setting.keyDescription(key, 'desktop');
const descriptionComp = descriptionText ? <div style={descriptionStyle}>{descriptionText}</div> : null;
const descriptionComp = this.renderDescription(this.props.themeId, descriptionText);
if (md.isEnum) {
if (settingKeyToControl[key]) {
const SettingComponent = settingKeyToControl[key];
return (
<div key={key} style={rowStyle}>
{this.renderLabel(this.props.themeId, md.label())}
{this.renderDescription(this.props.themeId, md.description ? md.description() : null)}
<SettingComponent
metadata={md}
value={value}
themeId={this.props.themeId}
onChange={(event: any) => {
updateSettingValue(key, event.value);
}}
/>
</div>
);
} else if (md.isEnum) {
const items = [];
const settingOptions = md.options();
const array = this.keyValueToArray(settingOptions);
@ -568,12 +619,33 @@ class ConfigScreenComponent extends React.Component<any, any> {
return output;
}
onApplyClick() {
shared.saveSettings(this);
private restartMessage() {
return _('The application must be restarted for these changes to take effect.');
}
onSaveClick() {
private async restartApp() {
await Setting.saveAll();
bridge().restart();
}
private async checkNeedRestart() {
if (this.state.needRestart) {
const doItNow = await bridge().showConfirmMessageBox(this.restartMessage(), {
buttons: [_('Do it now'), _('Later')],
});
if (doItNow) await this.restartApp();
}
}
async onApplyClick() {
shared.saveSettings(this);
await this.checkNeedRestart();
}
async onSaveClick() {
shared.saveSettings(this);
await this.checkNeedRestart();
this.props.dispatch({ type: 'NAV_BACK' });
}
@ -621,6 +693,13 @@ class ConfigScreenComponent extends React.Component<any, any> {
const sections = shared.settingsSections({ device: 'desktop', settings });
const needRestartComp: any = this.state.needRestart ? (
<div style={{ ...theme.textStyle, padding: 10, paddingLeft: 24, backgroundColor: theme.warningBackgroundColor, color: theme.color }}>
{this.restartMessage()}
<a style={{ ...theme.urlStyle, marginLeft: 10 }} href="#" onClick={() => { this.restartApp(); }}>{_('Restart now')}</a>
</div>
) : null;
return (
<div style={{ display: 'flex', flexDirection: 'row' }}>
<SideBar
@ -630,6 +709,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
/>
<div style={style}>
{screenComp}
{needRestartComp}
<div style={containerStyle}>{settingComps}</div>
<ButtonBar
hasChanges={hasChanges}

View File

@ -0,0 +1,255 @@
import * as React from 'react';
import { useCallback, useMemo } 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 ToggleButton from '../../lib/ToggleButton/ToggleButton';
import Button, { ButtonLevel } from '../../Button/Button';
import bridge from '../../../services/bridge';
import produce from 'immer';
const Root = styled.div`
display: flex;
flex-direction: column;
`;
const TableRoot = styled.div`
display: flex;
flex-wrap: wrap;
`;
const InstallButton = styled(Button)`
margin-bottom: 10px;
`;
const CellRoot = styled.div`
display: flex;
background-color: ${props => props.theme.backgroundColor};
flex-direction: column;
align-items: flex-start;
padding: 15px;
border: 1px solid ${props => props.theme.dividerColor};
border-radius: 6px;
width: 250px;
margin-right: 20px;
margin-bottom: 20px;
box-shadow: 1px 1px 3px rgba(0,0,0,0.2);
`;
const CellTop = styled.div`
display: flex;
flex-direction: row;
width: 100%;
margin-bottom: 10px;
`;
const CellContent = styled.div`
display: flex;
margin-bottom: 10px;
flex: 1;
`;
const CellFooter = styled.div`
display: flex;
flex-direction: row;
`;
const DevModeLabel = styled.div`
border: 1px solid ${props => props.theme.color};
border-radius: 4px;
padding: 4px 6px;
font-size: ${props => props.theme.fontSize * 0.75}px;
color: ${props => props.theme.color};
`;
const StyledName = styled.div`
font-family: ${props => props.theme.fontFamily};
color: ${props => props.theme.color};
font-size: ${props => props.theme.fontSize}px;
font-weight: bold;
flex: 1;
`;
const StyledDescription = styled.div`
font-family: ${props => props.theme.fontFamily};
color: ${props => props.theme.colorFaded};
font-size: ${props => props.theme.fontSize}px;
line-height: 1.6em;
`;
interface Props {
value: any;
themeId: number;
onChange: Function;
}
interface CellProps {
item: PluginItem;
themeId: number;
onToggle: Function;
onDelete: Function;
}
interface PluginItem {
id: string;
name: string;
description: string;
enabled: boolean;
deleted: boolean;
devMode: boolean;
}
function Cell(props: CellProps) {
const { item } = props;
// For plugins in dev mode things like enabling/disabling or
// uninstalling them doesn't make sense, as that should be done by
// adding/removing them from wherever they were loaded from.
function renderToggleButton() {
if (item.devMode) {
return <DevModeLabel>DEV</DevModeLabel>;
}
return <ToggleButton
themeId={props.themeId}
value={item.enabled}
onToggle={() => props.onToggle({ item: props.item })}
/>;
}
function renderFooter() {
if (item.devMode) return null;
return (
<CellFooter>
<Button level={ButtonLevel.Secondary} onClick={() => props.onDelete({ item: props.item })} title={_('Delete')}/>
<div style={{ display: 'flex', flex: 1 }}/>
</CellFooter>
);
}
return (
<CellRoot>
<CellTop>
<StyledName mb={'5px'}>{item.name} {item.deleted ? '(Deleted)' : ''}</StyledName>
{renderToggleButton()}
</CellTop>
<CellContent>
<StyledDescription>{item.description}</StyledDescription>
</CellContent>
{renderFooter()}
</CellRoot>
);
}
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 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]);
const onInstall = useCallback(async () => {
const result = bridge().showOpenDialog({
filters: [{ name: 'Joplin Plugin Archive', extensions: ['jpl'] }],
});
const filePath = result && result.length ? result[0] : null;
if (!filePath) return;
const plugin = await pluginService.installPlugin(filePath);
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
draft[plugin.manifest.id] = defaultPluginSetting();
});
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
}, [pluginSettings, props.onChange]);
function renderCells(items: PluginItem[]) {
const output = [];
for (const item of items) {
if (item.deleted) continue;
output.push(<Cell
key={item.id}
item={item}
themeId={props.themeId}
onDelete={onDelete}
onToggle={onToggle}
/>);
}
return output;
}
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
return (
<Root>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<InstallButton level={ButtonLevel.Primary} onClick={onInstall} title={_('Install plugin')}/>
<div style={{ display: 'flex', flex: 1 }}/>
</div>
<TableRoot>
{renderCells(pluginItems)}
</TableRoot>
</Root>
);
}

View File

@ -6,7 +6,7 @@ const { themeStyle } = require('@joplin/lib/theme');
const { _ } = require('@joplin/lib/locale');
const time = require('@joplin/lib/time').default;
const shim = require('@joplin/lib/shim').default;
const dialogs = require('./dialogs');
const dialogs = require('./dialogs').default;
const shared = require('@joplin/lib/components/shared/encryption-config-shared.js');
const bridge = require('electron').remote.require('./bridge').default;

View File

@ -22,6 +22,7 @@ import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
import CommandService from '@joplin/lib/services/CommandService';
import { themeStyle } from '@joplin/lib/theme';
import { ThemeAppearance } from '@joplin/lib/themes/type';
import dialogs from '../../../dialogs';
const Note = require('@joplin/lib/models/Note.js');
const { clipboard } = require('electron');
@ -29,7 +30,6 @@ const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const { reg } = require('@joplin/lib/registry.js');
const dialogs = require('../../../dialogs');
const menuUtils = new MenuUtils(CommandService.instance());

View File

@ -77,19 +77,19 @@ for (let i = 0; i < topLanguages.length; i++) {
}
export interface EditorProps {
value: string,
searchMarkers: any,
mode: string,
style: any,
codeMirrorTheme: any,
readOnly: boolean,
autoMatchBraces: boolean,
keyMap: string,
plugins: PluginStates,
onChange: any,
onScroll: any,
onEditorContextMenu: any,
onEditorPaste: any,
value: string;
searchMarkers: any;
mode: string;
style: any;
codeMirrorTheme: any;
readOnly: boolean;
autoMatchBraces: boolean;
keyMap: string;
plugins: PluginStates;
onChange: any;
onScroll: any;
onEditorContextMenu: any;
onEditorPaste: any;
}
function Editor(props: EditorProps, ref: any) {

View File

@ -4,6 +4,7 @@ const { buildStyle } = require('@joplin/lib/theme');
export default function styles(props: NoteBodyEditorProps) {
return buildStyle(['TinyMCE', props.style.width, props.style.height], props.themeId, (theme: any) => {
const extraToolbarContainer = {
boxSizing: 'content-box',
backgroundColor: theme.backgroundColor3,
display: 'flex',
flexDirection: 'row',

View File

@ -32,6 +32,10 @@ interface Props {
}
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
}
div, span, a {
/*color: ${(props: any) => props.theme.color};*/
/*font-size: ${(props: any) => props.theme.fontSize}px;*/

View File

@ -1,20 +1,20 @@
const smalltalk = require('smalltalk');
class Dialogs {
async alert(message, title = '') {
async alert(message: string, title = '') {
await smalltalk.alert(title, message);
}
async confirm(message, title = '') {
async confirm(message: string, title = '', options: any = {}) {
try {
await smalltalk.confirm(title, message);
await smalltalk.confirm(title, message, options);
return true;
} catch (error) {
return false;
}
}
async prompt(message, title = '', defaultValue = '', options = null) {
async prompt(message: string, title = '', defaultValue = '', options: any = null) {
if (options === null) options = {};
try {
@ -28,4 +28,4 @@ class Dialogs {
const dialogs = new Dialogs();
module.exports = dialogs;
export default dialogs;

View File

@ -0,0 +1,37 @@
import { themeStyle } from '@joplin/lib/theme';
import * as React from 'react';
const ReactToggleButton = require('react-toggle-button');
const Color = require('color');
interface Props {
value: boolean;
onToggle: Function;
themeId: number;
}
export default function(props: Props) {
const theme = themeStyle(props.themeId);
return (
<ReactToggleButton
value={props.value}
onToggle={props.onToggle}
colors={{
activeThumb: {
base: Color(theme.color5).rgb().string(),
},
active: {
base: Color(theme.backgroundColor5).alpha(0.7).rgb().string(),
},
}}
trackStyle={{
opacity: props.value ? 1 : 0.3,
}}
thumbStyle={{
opacity: props.value ? 1 : 0.5,
}}
inactiveLabel=""
activeLabel=""
/>
);
}

View File

@ -5134,11 +5134,6 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.4.tgz",
"integrity": "sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg=="
},
"currify": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/currify/-/currify-2.0.6.tgz",
"integrity": "sha512-F0lbcoBkA2FMcejFeHJkDEhQ1AvVkTpkn9PMzJch+7mHy5WdteZ9t+nhT6cOdga4uRay3rjvprgp8tUkixFy8w=="
},
"d": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
@ -5979,9 +5974,9 @@
}
},
"es6-promise": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz",
"integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng=="
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-symbol": {
"version": "3.1.3",
@ -6668,11 +6663,6 @@
"dev": true,
"optional": true
},
"fullstore": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fullstore/-/fullstore-1.1.0.tgz",
"integrity": "sha512-XNlCWr3KBIL97G8mTR+dZ/J648ECCffflfFRgZW3Zm7pO0PYnH/ZCbwZjV1Dw4LrrDdhV6gnayiIcmdIY4JTsw=="
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -12239,6 +12229,23 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-motion": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz",
"integrity": "sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==",
"requires": {
"performance-now": "^0.2.0",
"prop-types": "^15.5.8",
"raf": "^3.1.0"
},
"dependencies": {
"performance-now": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
"integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU="
}
}
},
"react-onclickoutside": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz",
@ -12302,6 +12309,15 @@
}
}
},
"react-toggle-button": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/react-toggle-button/-/react-toggle-button-2.2.0.tgz",
"integrity": "sha1-obkhQ6oN9BRkL8sUHwh59UW8Wok=",
"requires": {
"prop-types": "^15.6.0",
"react-motion": "^0.5.2"
}
},
"react-tooltip": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.10.0.tgz",
@ -13143,6 +13159,18 @@
"currify": "^2.0.3",
"es6-promise": "^4.1.1",
"fullstore": "^1.0.0"
},
"dependencies": {
"currify": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/currify/-/currify-2.0.6.tgz",
"integrity": "sha512-F0lbcoBkA2FMcejFeHJkDEhQ1AvVkTpkn9PMzJch+7mHy5WdteZ9t+nhT6cOdga4uRay3rjvprgp8tUkixFy8w=="
},
"fullstore": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fullstore/-/fullstore-1.1.0.tgz",
"integrity": "sha512-XNlCWr3KBIL97G8mTR+dZ/J648ECCffflfFRgZW3Zm7pO0PYnH/ZCbwZjV1Dw4LrrDdhV6gnayiIcmdIY4JTsw=="
}
}
},
"snapdragon": {

View File

@ -144,6 +144,7 @@
"react-dom": "16.9.0",
"react-redux": "^5.0.7",
"react-select": "^2.4.3",
"react-toggle-button": "^2.2.0",
"react-tooltip": "^3.10.0",
"redux": "^3.7.2",
"reselect": "^4.0.0",

View File

@ -100,9 +100,11 @@ export default class PluginRunner extends BasePluginRunner {
slashes: true,
})}?pluginId=${encodeURIComponent(plugin.id)}&pluginScript=${encodeURIComponent(`file://${scriptPath}`)}`);
pluginWindow.webContents.once('dom-ready', () => {
pluginWindow.webContents.openDevTools({ mode: 'detach' });
});
if (plugin.devMode) {
pluginWindow.webContents.once('dom-ready', () => {
pluginWindow.webContents.openDevTools({ mode: 'detach' });
});
}
ipcRenderer.on('pluginMessage', async (_event: any, message: PluginMessage) => {
if (message.target !== PluginMessageTarget.MainWindow) return;

View File

@ -92,6 +92,15 @@ shared.updateSettingValue = function(comp, key, value) {
});
};
shared.scheduleSaveSettings = function(comp) {
if (shared.scheduleSaveSettingsIID) clearTimeout(shared.scheduleSaveSettingsIID);
shared.scheduleSaveSettingsIID = setTimeout(() => {
shared.scheduleSaveSettingsIID = null;
shared.saveSettings(comp);
}, 100);
};
shared.saveSettings = function(comp) {
for (const key in comp.state.settings) {
if (!comp.state.settings.hasOwnProperty(key)) continue;

View File

@ -33,7 +33,7 @@ export interface SettingItem {
isEnum?: boolean;
section?: string;
label?(): string;
description?(appType: string): string;
description?: Function;
options?(): any;
appTypes?: string[];
show?(settings: any): boolean;
@ -44,7 +44,9 @@ export interface SettingItem {
maximum?: number;
step?: number;
onClick?(): void;
unitLabel?(value: any): string;
unitLabel?: Function;
needRestart?: boolean;
autoSave?: boolean;
}
interface SettingItems {
@ -554,6 +556,17 @@ class Setting extends BaseModel {
},
},
'plugins.states': {
value: '',
type: SettingItemType.Object,
section: 'plugins',
public: true,
appTypes: ['desktop'],
label: () => _('Plugins'),
needRestart: true,
autoSave: true,
},
'plugins.devPluginPaths': {
value: '',
type: SettingItemType.String,
@ -816,7 +829,7 @@ class Setting extends BaseModel {
minimum: 1,
maximum: 365 * 2,
step: 1,
unitLabel: (value = null) => {
unitLabel: (value: number = null) => {
return value === null ? _('days') : _('%d days', value);
},
label: () => _('Keep note history for'),

View File

@ -21,19 +21,17 @@ interface ContentScripts {
export default class Plugin {
private id_: string;
private baseDir_: string;
private manifest_: PluginManifest;
private scriptText_: string;
private enabled_: boolean = true;
private logger_: Logger = null;
private viewControllers_: ViewControllers = {};
private contentScripts_: ContentScripts = {};
private dispatch_: Function;
private eventEmitter_: any;
private devMode_: boolean = false;
constructor(id: string, baseDir: string, manifest: PluginManifest, scriptText: string, logger: Logger, dispatch: Function) {
this.id_ = id;
constructor(baseDir: string, manifest: PluginManifest, scriptText: string, logger: Logger, dispatch: Function) {
this.baseDir_ = shim.fsDriver().resolve(baseDir);
this.manifest_ = manifest;
this.scriptText_ = scriptText;
@ -43,15 +41,15 @@ export default class Plugin {
}
public get id(): string {
return this.id_;
return this.manifest.id;
}
public get enabled(): boolean {
return this.enabled_;
public get devMode(): boolean {
return this.devMode_;
}
public set enabled(v: boolean) {
this.enabled_ = v;
public set devMode(v: boolean) {
this.devMode_ = v;
}
public get manifest(): PluginManifest {

View File

@ -4,17 +4,42 @@ import Global from './api/Global';
import BasePluginRunner from './BasePluginRunner';
import BaseService from '../BaseService';
import shim from '../../shim';
import { rtrimSlashes } from '../../path-utils';
import { filename, dirname, rtrimSlashes, basename } from '../../path-utils';
import Setting from '../../models/Setting';
const compareVersions = require('compare-versions');
const { filename, dirname } = require('../../path-utils');
const uslug = require('uslug');
const md5File = require('md5-file/promise');
interface Plugins {
// Plugin data is split into two:
//
// - First there's the service `plugins` property, which contains the
// plugin static data, as loaded from the plugin file or directory. For
// example, the plugin ID, the manifest, the script files, etc.
//
// - Secondly, there's the `PluginSettings` data, which is dynamic and is
// used for example to enable or disable a plugin. Its state is saved to
// the user's settings.
export interface Plugins {
[key: string]: Plugin;
}
export interface PluginSetting {
enabled: boolean;
deleted: boolean;
}
export function defaultPluginSetting(): PluginSetting {
return {
enabled: true,
deleted: false,
};
}
export interface PluginSettings {
[pluginId: string]: PluginSetting;
}
function makePluginId(source: string): string {
// https://www.npmjs.com/package/slug#options
return uslug(source).substr(0,32);
@ -49,15 +74,42 @@ export default class PluginService extends BaseService {
return this.plugins_;
}
private setPluginAt(pluginId: string, plugin: Plugin) {
this.plugins_ = {
...this.plugins_,
[pluginId]: plugin,
};
}
private deletePluginAt(pluginId: string) {
if (!this.plugins_[pluginId]) return;
this.plugins_ = { ...this.plugins_ };
delete this.plugins_[pluginId];
}
public pluginById(id: string): Plugin {
if (!this.plugins_[id]) throw new Error(`Plugin not found: ${id}`);
return this.plugins_[id];
}
// public allPluginIds(): string[] {
// return Object.keys(this.plugins_);
// }
public unserializePluginSettings(settings: any): PluginSettings {
const output = { ...settings };
for (const pluginId in output) {
output[pluginId] = {
...defaultPluginSetting(),
...output[pluginId],
};
}
return output;
}
public serializePluginSettings(settings: PluginSettings): any {
return JSON.stringify(settings);
}
private async parsePluginJsBundle(jsBundleString: string) {
const scriptText = jsBundleString;
@ -146,7 +198,7 @@ export default class PluginService extends BaseService {
}
}
private async loadPluginFromPath(path: string): Promise<Plugin> {
public async loadPluginFromPath(path: string): Promise<Plugin> {
path = rtrimSlashes(path);
const fsDriver = shim.fsDriver();
@ -189,28 +241,8 @@ export default class PluginService extends BaseService {
}
const manifest = manifestFromObject(manifestObj);
const pluginId = manifest.id;
// After transforming the plugin path to an ID, multiple plugins might end up with the same ID. For
// example "MyPlugin" and "myplugin" would have the same ID. Technically it's possible to have two
// such folders but to keep things sane we disallow it.
if (this.plugins_[pluginId]) throw new Error(`There is already a plugin with this ID: ${pluginId}`);
const plugin = new Plugin(pluginId, baseDir, manifest, scriptText, this.logger(), (action: any) => this.store_.dispatch(action));
if (compareVersions(this.appVersion_, manifest.app_min_version) < 0) {
this.logger().info(`PluginService: Plugin "${pluginId}" was disabled because it requires a newer version of Joplin.`, manifest);
plugin.enabled = false;
} else {
this.store_.dispatch({
type: 'PLUGIN_ADD',
plugin: {
id: pluginId,
views: {},
contentScripts: {},
},
});
}
const plugin = new Plugin(baseDir, manifest, scriptText, this.logger(), (action: any) => this.store_.dispatch(action));
for (const msg of deprecationNotices) {
plugin.deprecationNotice('1.5', msg);
@ -219,14 +251,24 @@ export default class PluginService extends BaseService {
return plugin;
}
public async loadAndRunPlugins(pluginDirOrPaths: string | string[]) {
private pluginEnabled(settings: PluginSettings, pluginId: string): boolean {
if (!settings[pluginId]) return true;
return settings[pluginId].enabled !== false;
}
public async loadAndRunPlugins(pluginDirOrPaths: string | string[], settings: PluginSettings, devMode: boolean = false) {
let pluginPaths = [];
if (Array.isArray(pluginDirOrPaths)) {
pluginPaths = pluginDirOrPaths;
} else {
pluginPaths = (await shim.fsDriver().readDirStats(pluginDirOrPaths))
.filter((stat: any) => (stat.isDirectory() || stat.path.toLowerCase().endsWith('.js')))
.filter((stat: any) => {
if (stat.isDirectory()) return true;
if (stat.path.toLowerCase().endsWith('.js')) return true;
if (stat.path.toLowerCase().endsWith('.jpl')) return true;
return false;
})
.map((stat: any) => `${pluginDirOrPaths}/${stat.path}`);
}
@ -238,6 +280,21 @@ export default class PluginService extends BaseService {
try {
const plugin = await this.loadPluginFromPath(pluginPath);
// After transforming the plugin path to an ID, multiple plugins might end up with the same ID. For
// example "MyPlugin" and "myplugin" would have the same ID. Technically it's possible to have two
// such folders but to keep things sane we disallow it.
if (this.plugins_[plugin.id]) throw new Error(`There is already a plugin with this ID: ${plugin.id}`);
this.setPluginAt(plugin.id, plugin);
if (!this.pluginEnabled(settings, plugin.id)) {
this.logger().info(`PluginService: Not running disabled plugin: "${plugin.id}"`);
continue;
}
plugin.devMode = devMode;
await this.runPlugin(plugin);
} catch (error) {
this.logger().error(`PluginService: Could not load plugin: ${pluginPath}`, error);
@ -246,22 +303,71 @@ export default class PluginService extends BaseService {
}
public async runPlugin(plugin: Plugin) {
this.plugins_[plugin.id] = plugin;
if (compareVersions(this.appVersion_, plugin.manifest.app_min_version) < 0) {
throw new Error(`PluginService: Plugin "${plugin.id}" was disabled because it requires Joplin version ${plugin.manifest.app_min_version} and current version is ${this.appVersion_}.`);
} else {
this.store_.dispatch({
type: 'PLUGIN_ADD',
plugin: {
id: plugin.id,
views: {},
contentScripts: {},
},
});
}
const pluginApi = new Global(this.logger(), this.platformImplementation_, plugin, this.store_);
return this.runner_.run(plugin, pluginApi);
}
// public async handleDisabledPlugins() {
// const enabledPlugins = this.allPluginIds();
// const v = await this.kvStore_.value<string>('pluginService.lastEnabledPlugins');
// const lastEnabledPlugins = v ? JSON.parse(v) : [];
public async installPlugin(jplPath: string): Promise<Plugin> {
this.logger().info(`PluginService: Installing plugin: "${jplPath}"`);
// const disabledPlugins = [];
// for (const id of lastEnabledPlugins) {
// if (!enabledPlugins.includes(id)) disabledPlugins.push(id);
// }
const destPath = `${Setting.value('pluginDir')}/${basename(jplPath)}`;
await shim.fsDriver().copy(jplPath, destPath);
const plugin = await this.loadPluginFromPath(destPath);
if (!this.plugins_[plugin.id]) this.setPluginAt(plugin.id, plugin);
return plugin;
}
// await this.kvStore_.setValue('pluginService.lastEnabledPlugins', JSON.stringify(enabledPlugins));
// }
private async pluginPath(pluginId: string) {
const stats = await shim.fsDriver().readDirStats(Setting.value('pluginDir'), { recursive: false });
for (const stat of stats) {
if (filename(stat.path) === pluginId) {
return `${Setting.value('pluginDir')}/${stat.path}`;
}
}
return null;
}
public async uninstallPlugin(pluginId: string) {
this.logger().info(`PluginService: Uninstalling plugin: "${pluginId}"`);
const path = await this.pluginPath(pluginId);
if (!path) {
// Plugin might have already been deleted
this.logger().error(`PluginService: Could not find plugin path to uninstall - nothing will be done: ${pluginId}`);
} else {
await shim.fsDriver().remove(path);
}
this.deletePluginAt(pluginId);
}
public async uninstallPlugins(settings: PluginSettings): Promise<PluginSettings> {
let newSettings = settings;
for (const pluginId in settings) {
if (settings[pluginId].deleted) {
await this.uninstallPlugin(pluginId);
newSettings = { ...settings };
delete newSettings[pluginId];
}
}
return newSettings;
}
}

View File

@ -68,9 +68,9 @@ function slugify(s: string): string {
const inMemoryCache = new InMemoryCache(20);
export interface ExtraRendererRule {
id: string,
module: any,
assetPath: string,
id: string;
module: any;
assetPath: string;
}
export interface Options {