1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-06 09:19:22 +02:00

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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