1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-08 23:07:32 +02:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Laurent Cozic
28be0dbd36 update 2020-11-19 12:28:16 +00:00
Laurent Cozic
cd406cba51 update 2020-11-18 22:58:09 +00:00
Laurent Cozic
57c0305ad8 gui 2020-11-18 17:36:04 +00:00
22 changed files with 720 additions and 122 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.d.ts
packages/app-cli/tests/support/plugins/events/src/index.js 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/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.d.ts
packages/app-cli/tests/support/plugins/json_export/api/index.js packages/app-cli/tests/support/plugins/json_export/api/index.js
packages/app-cli/tests/support/plugins/json_export/api/index.js.map 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.d.ts
packages/app-desktop/gui/ConfigScreen/SideBar.js packages/app-desktop/gui/ConfigScreen/SideBar.js
packages/app-desktop/gui/ConfigScreen/SideBar.js.map 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.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map 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.d.ts
packages/app-desktop/gui/ToolbarButton/styles/index.js packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarButton/styles/index.js.map 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.d.ts
packages/app-desktop/gui/hooks/useEffectDebugger.js packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useEffectDebugger.js.map 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.d.ts
packages/app-desktop/gui/hooks/usePropsDebugger.js packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/hooks/usePropsDebugger.js.map 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.d.ts
packages/app-desktop/gui/menuCommandNames.js packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map 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.d.ts
packages/app-cli/tests/support/plugins/events/src/index.js 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/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.d.ts
packages/app-cli/tests/support/plugins/json_export/api/index.js packages/app-cli/tests/support/plugins/json_export/api/index.js
packages/app-cli/tests/support/plugins/json_export/api/index.js.map 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.d.ts
packages/app-desktop/gui/ConfigScreen/SideBar.js packages/app-desktop/gui/ConfigScreen/SideBar.js
packages/app-desktop/gui/ConfigScreen/SideBar.js.map 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.d.ts
packages/app-desktop/gui/DropboxLoginScreen.js packages/app-desktop/gui/DropboxLoginScreen.js
packages/app-desktop/gui/DropboxLoginScreen.js.map 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.d.ts
packages/app-desktop/gui/ToolbarButton/styles/index.js packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarButton/styles/index.js.map 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.d.ts
packages/app-desktop/gui/hooks/useEffectDebugger.js packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useEffectDebugger.js.map 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.d.ts
packages/app-desktop/gui/hooks/usePropsDebugger.js packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/hooks/usePropsDebugger.js.map 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.d.ts
packages/app-desktop/gui/menuCommandNames.js packages/app-desktop/gui/menuCommandNames.js
packages/app-desktop/gui/menuCommandNames.js.map packages/app-desktop/gui/menuCommandNames.js.map

View File

@@ -13,6 +13,7 @@
"_vieux/": true, "_vieux/": true,
".gitignore": true, ".gitignore": true,
".eslintignore": true, ".eslintignore": true,
"**/*.jpl": true,
"./packages/app-cli/**/*.*~": true, "./packages/app-cli/**/*.*~": true,
"./packages/app-cli/**/*.mo": true, "./packages/app-cli/**/*.mo": true,
"./packages/app-cli/**/build/": true, "./packages/app-cli/**/build/": true,
@@ -55,7 +56,6 @@
"./packages/app-desktop/**/*.min.js": true, "./packages/app-desktop/**/*.min.js": true,
"./packages/app-desktop/**/dist/": true, "./packages/app-desktop/**/dist/": true,
"./packages/app-desktop/**/gui/note-viewer/pluginAssets/": true, "./packages/app-desktop/**/gui/note-viewer/pluginAssets/": true,
"./packages/app-desktop/**/lib/": true,
"./packages/app-desktop/**/node_modules/": true, "./packages/app-desktop/**/node_modules/": true,
"./packages/app-desktop/**/packageInfo.js": true, "./packages/app-desktop/**/packageInfo.js": true,
"./packages/app-desktop/**/pluginAssets/": true, "./packages/app-desktop/**/pluginAssets/": true,
@@ -242,7 +242,6 @@
"packages/app-desktop/**/*.min.js": true, "packages/app-desktop/**/*.min.js": true,
"packages/app-desktop/**/dist/": true, "packages/app-desktop/**/dist/": true,
"packages/app-desktop/**/gui/note-viewer/pluginAssets/": true, "packages/app-desktop/**/gui/note-viewer/pluginAssets/": true,
"packages/app-desktop/**/lib/": true,
"packages/app-desktop/**/node_modules/": true, "packages/app-desktop/**/node_modules/": true,
"packages/app-desktop/**/packageInfo.js": true, "packages/app-desktop/**/packageInfo.js": true,
"packages/app-desktop/**/pluginAssets/": 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 MdToHtml from '@joplin/renderer/MdToHtml';
import shim from '@joplin/lib/shim'; 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 Note = require('@joplin/lib/models/Note');
const Folder = require('@joplin/lib/models/Folder'); 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 () => { it('should load and run a simple plugin', asyncTest(async () => {
const service = newPluginService(); const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple`]); await service.loadAndRunPlugins([`${testPluginDir}/simple`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError(); 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 () => { it('should load and run a simple plugin and handle trailing slash', asyncTest(async () => {
const service = newPluginService(); const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple/`]); await service.loadAndRunPlugins([`${testPluginDir}/simple/`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError(); expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
})); }));
it('should load and run a plugin that uses external packages', asyncTest(async () => { it('should load and run a plugin that uses external packages', asyncTest(async () => {
const service = newPluginService(); const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`]); await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`], {});
expect(() => service.pluginById('org.joplinapp.plugins.ExternalModuleDemo')).not.toThrowError(); expect(() => service.pluginById('org.joplinapp.plugins.ExternalModuleDemo')).not.toThrowError();
const allFolders = await Folder.all(); const allFolders = await Folder.all();
@@ -78,7 +78,7 @@ describe('services_PluginService', function() {
it('should load multiple plugins from a directory', asyncTest(async () => { it('should load multiple plugins from a directory', asyncTest(async () => {
const service = newPluginService(); const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`); await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`, {});
const plugin1 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo1'); const plugin1 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo1');
const plugin2 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo2'); 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 () => { it('should load plugins from JS bundle files', asyncTest(async () => {
const service = newPluginService(); const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`); await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`, {});
expect(!!service.pluginById('org.joplinapp.plugins.JsBundleDemo')).toBe(true); expect(!!service.pluginById('org.joplinapp.plugins.JsBundleDemo')).toBe(true);
expect((await Folder.all()).length).toBe(1); expect((await Folder.all()).length).toBe(1);
})); }));
it('should load plugins from JPL archive', asyncTest(async () => { it('should load plugins from JPL archive', asyncTest(async () => {
const service = newPluginService(); 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(!!service.pluginById('org.joplinapp.FirstJplPlugin')).toBe(true);
expect((await Folder.all()).length).toBe(1); expect((await Folder.all()).length).toBe(1);
})); }));
@@ -248,9 +248,15 @@ describe('services_PluginService', function() {
]; ];
for (const testCase of testCases) { for (const testCase of testCases) {
const [appVersion, expected] = testCase; const [appVersion, hasNoError] = testCase;
const plugin = await newPluginService(appVersion as string).loadPluginFromJsBundle('', pluginScript); const service = newPluginService(appVersion as string);
expect(plugin.enabled).toBe(expected as boolean); 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/ node_modules/
packageInfo.js packageInfo.js
dist/ dist/
lib/
*.min.js *.min.js
.DS_Store .DS_Store
gui/note-viewer/pluginAssets/ gui/note-viewer/pluginAssets/

View File

@@ -495,12 +495,25 @@ class Application extends BaseApplication {
pluginLogger.addTarget(TargetType.Console, { prefix: 'Plugin Service:' }); pluginLogger.addTarget(TargetType.Console, { prefix: 'Plugin Service:' });
pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO); pluginLogger.setLevel(Setting.value('env') == 'dev' ? Logger.LEVEL_DEBUG : Logger.LEVEL_INFO);
const service = PluginService.instance();
const pluginRunner = new PluginRunner(); const pluginRunner = new PluginRunner();
PluginService.instance().setLogger(pluginLogger); service.setLogger(pluginLogger);
PluginService.instance().initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store()); 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 { 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) { } catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error); this.logger().error(`There was an error loading plugins from ${Setting.value('pluginDir')}:`, error);
} }
@@ -508,12 +521,12 @@ class Application extends BaseApplication {
try { try {
if (Setting.value('plugins.devPluginPaths')) { if (Setting.value('plugins.devPluginPaths')) {
const paths = Setting.value('plugins.devPluginPaths').split(',').map((p: string) => p.trim()); 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 // Also load dev plugins that have passed via command line arguments
if (Setting.value('startupDevPlugins')) { if (Setting.value('startupDevPlugins')) {
await PluginService.instance().loadAndRunPlugins(Setting.value('startupDevPlugins')); await service.loadAndRunPlugins(Setting.value('startupDevPlugins'), pluginSettings, true);
} }
} catch (error) { } catch (error) {
this.logger().error(`There was an error loading plugins from ${Setting.value('plugins.devPluginPaths')}:`, 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())); // console.info(CommandService.instance().commandsToMarkdownTable(this.store().getState()));
// }, 2000); // }, 2000);
// this.dispatch({
// type: 'NAV_GO',
// routeName: 'Config',
// props: {
// defaultSection: 'plugins',
// },
// });
return null; return null;
} }

View File

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

View File

@@ -3,17 +3,23 @@ import SideBar from './SideBar';
import ButtonBar from './ButtonBar'; import ButtonBar from './ButtonBar';
import Button, { ButtonLevel } from '../Button/Button'; import Button, { ButtonLevel } from '../Button/Button';
import { _ } from '@joplin/lib/locale'; 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 { connect } = require('react-redux');
const Setting = require('@joplin/lib/models/Setting').default;
const { themeStyle } = require('@joplin/lib/theme'); const { themeStyle } = require('@joplin/lib/theme');
const pathUtils = require('@joplin/lib/path-utils'); const pathUtils = require('@joplin/lib/path-utils');
const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry'); const SyncTargetRegistry = require('@joplin/lib/SyncTargetRegistry');
const shared = require('@joplin/lib/components/shared/config-shared.js'); const shared = require('@joplin/lib/components/shared/config-shared.js');
const bridge = require('electron').remote.require('./bridge').default;
const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min'); const { EncryptionConfigScreen } = require('../EncryptionConfigScreen.min');
const { ClipperConfigScreen } = require('../ClipperConfigScreen.min'); const { ClipperConfigScreen } = require('../ClipperConfigScreen.min');
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen'); const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
const settingKeyToControl: any = {
'plugins.states': control_PluginsStates,
};
class ConfigScreenComponent extends React.Component<any, any> { class ConfigScreenComponent extends React.Component<any, any> {
rowStyle_: any = null; rowStyle_: any = null;
@@ -27,6 +33,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
selectedSectionName: 'general', selectedSectionName: 'general',
screenName: '', screenName: '',
changedSettingKeys: [], changedSettingKeys: [],
needRestart: false,
}; };
this.rowStyle_ = { this.rowStyle_ = {
@@ -41,6 +48,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.onCancelClick = this.onCancelClick.bind(this); this.onCancelClick = this.onCancelClick.bind(this);
this.onSaveClick = this.onSaveClick.bind(this); this.onSaveClick = this.onSaveClick.bind(this);
this.onApplyClick = this.onApplyClick.bind(this); this.onApplyClick = this.onApplyClick.bind(this);
this.renderLabel = this.renderLabel.bind(this);
this.renderDescription = this.renderDescription.bind(this);
} }
async checkSyncConfig_() { 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) { settingToComponent(key: string, value: any) {
const theme = themeStyle(this.props.themeId); const theme = themeStyle(this.props.themeId);
@@ -270,13 +313,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
marginBottom: theme.mainPadding, marginBottom: theme.mainPadding,
}; };
const labelStyle = Object.assign({}, theme.textStyle, { const labelStyle = this.labelStyle(this.props.themeId);
display: 'block',
color: theme.color,
fontSize: theme.fontSize * 1.083333,
fontWeight: 500,
marginBottom: theme.mainPadding / 4,
});
const subLabel = Object.assign({}, labelStyle, { const subLabel = Object.assign({}, labelStyle, {
display: 'block', display: 'block',
@@ -297,13 +334,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
backgroundColor: theme.backgroundColor, backgroundColor: theme.backgroundColor,
}; };
const descriptionStyle = Object.assign({}, theme.textStyle, {
color: theme.colorFaded,
marginTop: 5,
fontStyle: 'italic',
maxWidth: '70em',
});
const textInputBaseStyle = Object.assign({}, controlStyle, { const textInputBaseStyle = Object.assign({}, controlStyle, {
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
border: '1px solid', border: '1px solid',
@@ -318,18 +348,39 @@ class ConfigScreenComponent extends React.Component<any, any> {
}); });
const updateSettingValue = (key: string, value: any) => { const updateSettingValue = (key: string, value: any) => {
// console.info(key + ' = ' + value); const md = Setting.settingMetadata(key);
return shared.updateSettingValue(this, key, value); 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 md = Setting.settingMetadata(key);
const descriptionText = Setting.keyDescription(key, 'desktop'); 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 items = [];
const settingOptions = md.options(); const settingOptions = md.options();
const array = this.keyValueToArray(settingOptions); const array = this.keyValueToArray(settingOptions);
@@ -568,12 +619,33 @@ class ConfigScreenComponent extends React.Component<any, any> {
return output; return output;
} }
onApplyClick() { private restartMessage() {
shared.saveSettings(this); 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); shared.saveSettings(this);
await this.checkNeedRestart();
}
async onSaveClick() {
shared.saveSettings(this);
await this.checkNeedRestart();
this.props.dispatch({ type: 'NAV_BACK' }); 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 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 ( return (
<div style={{ display: 'flex', flexDirection: 'row' }}> <div style={{ display: 'flex', flexDirection: 'row' }}>
<SideBar <SideBar
@@ -630,6 +709,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
/> />
<div style={style}> <div style={style}>
{screenComp} {screenComp}
{needRestartComp}
<div style={containerStyle}>{settingComps}</div> <div style={containerStyle}>{settingComps}</div>
<ButtonBar <ButtonBar
hasChanges={hasChanges} 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 { _ } = require('@joplin/lib/locale');
const time = require('@joplin/lib/time').default; const time = require('@joplin/lib/time').default;
const shim = require('@joplin/lib/shim').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 shared = require('@joplin/lib/components/shared/encryption-config-shared.js');
const bridge = require('electron').remote.require('./bridge').default; 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 CommandService from '@joplin/lib/services/CommandService';
import { themeStyle } from '@joplin/lib/theme'; import { themeStyle } from '@joplin/lib/theme';
import { ThemeAppearance } from '@joplin/lib/themes/type'; import { ThemeAppearance } from '@joplin/lib/themes/type';
import dialogs from '../../../dialogs';
const Note = require('@joplin/lib/models/Note.js'); const Note = require('@joplin/lib/models/Note.js');
const { clipboard } = require('electron'); const { clipboard } = require('electron');
@@ -29,7 +30,6 @@ const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
const Menu = bridge().Menu; const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem; const MenuItem = bridge().MenuItem;
const { reg } = require('@joplin/lib/registry.js'); const { reg } = require('@joplin/lib/registry.js');
const dialogs = require('../../../dialogs');
const menuUtils = new MenuUtils(CommandService.instance()); const menuUtils = new MenuUtils(CommandService.instance());

View File

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

View File

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

View File

@@ -1,20 +1,20 @@
const smalltalk = require('smalltalk'); const smalltalk = require('smalltalk');
class Dialogs { class Dialogs {
async alert(message, title = '') { async alert(message:string, title = '') {
await smalltalk.alert(title, message); await smalltalk.alert(title, message);
} }
async confirm(message, title = '') { async confirm(message:string, title = '', options:any = {}) {
try { try {
await smalltalk.confirm(title, message); await smalltalk.confirm(title, message, options);
return true; return true;
} catch (error) { } catch (error) {
return false; return false;
} }
} }
async prompt(message, title = '', defaultValue = '', options = null) { async prompt(message:string, title = '', defaultValue = '', options:any = null) {
if (options === null) options = {}; if (options === null) options = {};
try { try {
@@ -28,4 +28,4 @@ class Dialogs {
const dialogs = new 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", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.4.tgz",
"integrity": "sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==" "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": { "d": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
@@ -5979,9 +5974,9 @@
} }
}, },
"es6-promise": { "es6-promise": {
"version": "4.1.1", "version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==" "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
}, },
"es6-symbol": { "es6-symbol": {
"version": "3.1.3", "version": "3.1.3",
@@ -6668,11 +6663,6 @@
"dev": true, "dev": true,
"optional": 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": { "function-bind": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "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", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "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": { "react-onclickoutside": {
"version": "6.7.1", "version": "6.7.1",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz", "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": { "react-tooltip": {
"version": "3.10.0", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.10.0.tgz", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-3.10.0.tgz",
@@ -13143,6 +13159,18 @@
"currify": "^2.0.3", "currify": "^2.0.3",
"es6-promise": "^4.1.1", "es6-promise": "^4.1.1",
"fullstore": "^1.0.0" "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": { "snapdragon": {

View File

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

View File

@@ -100,9 +100,11 @@ export default class PluginRunner extends BasePluginRunner {
slashes: true, slashes: true,
})}?pluginId=${encodeURIComponent(plugin.id)}&pluginScript=${encodeURIComponent(`file://${scriptPath}`)}`); })}?pluginId=${encodeURIComponent(plugin.id)}&pluginScript=${encodeURIComponent(`file://${scriptPath}`)}`);
pluginWindow.webContents.once('dom-ready', () => { if (plugin.devMode) {
pluginWindow.webContents.openDevTools({ mode: 'detach' }); pluginWindow.webContents.once('dom-ready', () => {
}); pluginWindow.webContents.openDevTools({ mode: 'detach' });
});
}
ipcRenderer.on('pluginMessage', async (_event: any, message: PluginMessage) => { ipcRenderer.on('pluginMessage', async (_event: any, message: PluginMessage) => {
if (message.target !== PluginMessageTarget.MainWindow) return; 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) { shared.saveSettings = function(comp) {
for (const key in comp.state.settings) { for (const key in comp.state.settings) {
if (!comp.state.settings.hasOwnProperty(key)) continue; if (!comp.state.settings.hasOwnProperty(key)) continue;

View File

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

View File

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

View File

@@ -4,17 +4,42 @@ import Global from './api/Global';
import BasePluginRunner from './BasePluginRunner'; import BasePluginRunner from './BasePluginRunner';
import BaseService from '../BaseService'; import BaseService from '../BaseService';
import shim from '../../shim'; import shim from '../../shim';
import { rtrimSlashes } from '../../path-utils'; import { filename, dirname, rtrimSlashes, basename } from '../../path-utils';
import Setting from '../../models/Setting'; import Setting from '../../models/Setting';
const compareVersions = require('compare-versions'); const compareVersions = require('compare-versions');
const { filename, dirname } = require('../../path-utils');
const uslug = require('uslug'); const uslug = require('uslug');
const md5File = require('md5-file/promise'); 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; [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 { function makePluginId(source: string): string {
// https://www.npmjs.com/package/slug#options // https://www.npmjs.com/package/slug#options
return uslug(source).substr(0,32); return uslug(source).substr(0,32);
@@ -49,15 +74,42 @@ export default class PluginService extends BaseService {
return this.plugins_; 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 { public pluginById(id: string): Plugin {
if (!this.plugins_[id]) throw new Error(`Plugin not found: ${id}`); if (!this.plugins_[id]) throw new Error(`Plugin not found: ${id}`);
return this.plugins_[id]; return this.plugins_[id];
} }
// public allPluginIds(): string[] { public unserializePluginSettings(settings: any): PluginSettings {
// return Object.keys(this.plugins_); 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) { private async parsePluginJsBundle(jsBundleString: string) {
const scriptText = jsBundleString; 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); path = rtrimSlashes(path);
const fsDriver = shim.fsDriver(); const fsDriver = shim.fsDriver();
@@ -189,28 +241,8 @@ export default class PluginService extends BaseService {
} }
const manifest = manifestFromObject(manifestObj); 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 const plugin = new Plugin(baseDir, manifest, scriptText, this.logger(), (action: any) => this.store_.dispatch(action));
// 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: {},
},
});
}
for (const msg of deprecationNotices) { for (const msg of deprecationNotices) {
plugin.deprecationNotice('1.5', msg); plugin.deprecationNotice('1.5', msg);
@@ -219,14 +251,24 @@ export default class PluginService extends BaseService {
return plugin; 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 = []; let pluginPaths = [];
if (Array.isArray(pluginDirOrPaths)) { if (Array.isArray(pluginDirOrPaths)) {
pluginPaths = pluginDirOrPaths; pluginPaths = pluginDirOrPaths;
} else { } else {
pluginPaths = (await shim.fsDriver().readDirStats(pluginDirOrPaths)) 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}`); .map((stat: any) => `${pluginDirOrPaths}/${stat.path}`);
} }
@@ -238,6 +280,21 @@ export default class PluginService extends BaseService {
try { try {
const plugin = await this.loadPluginFromPath(pluginPath); 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); await this.runPlugin(plugin);
} catch (error) { } catch (error) {
this.logger().error(`PluginService: Could not load plugin: ${pluginPath}`, 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) { 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_); const pluginApi = new Global(this.logger(), this.platformImplementation_, plugin, this.store_);
return this.runner_.run(plugin, pluginApi); return this.runner_.run(plugin, pluginApi);
} }
// public async handleDisabledPlugins() { public async installPlugin(jplPath: string): Promise<Plugin> {
// const enabledPlugins = this.allPluginIds(); this.logger().info(`PluginService: Installing plugin: "${jplPath}"`);
// const v = await this.kvStore_.value<string>('pluginService.lastEnabledPlugins');
// const lastEnabledPlugins = v ? JSON.parse(v) : [];
// const disabledPlugins = []; const destPath = `${Setting.value('pluginDir')}/${basename(jplPath)}`;
// for (const id of lastEnabledPlugins) { await shim.fsDriver().copy(jplPath, destPath);
// if (!enabledPlugins.includes(id)) disabledPlugins.push(id); 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;
}
} }