mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Add config screen to add, remove or enable, disable plugins
This commit is contained in:
parent
f36019c94d
commit
e57444dc32
@ -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
18
.gitignore
vendored
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
|
1
packages/app-desktop/.gitignore
vendored
1
packages/app-desktop/.gitignore
vendored
@ -1,7 +1,6 @@
|
||||
node_modules/
|
||||
packageInfo.js
|
||||
dist/
|
||||
lib/
|
||||
*.min.js
|
||||
.DS_Store
|
||||
gui/note-viewer/pluginAssets/
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
255
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.tsx
Normal file
255
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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;*/
|
||||
|
@ -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;
|
37
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.tsx
Normal file
37
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.tsx
Normal 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=""
|
||||
/>
|
||||
);
|
||||
}
|
54
packages/app-desktop/package-lock.json
generated
54
packages/app-desktop/package-lock.json
generated
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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'),
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user