mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Desktop: Add support for searching and installing plugins from repository
This commit is contained in:
parent
1700b29f7d
commit
6dc5a816e5
@ -326,9 +326,18 @@ 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/plugins/PluginBox.d.ts
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js.map
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.d.ts
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js.map
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.d.ts
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js.map
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.d.ts
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.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
|
||||||
@ -656,9 +665,6 @@ packages/app-desktop/gui/Root_UpgradeSyncTarget.js.map
|
|||||||
packages/app-desktop/gui/SearchBar/SearchBar.d.ts
|
packages/app-desktop/gui/SearchBar/SearchBar.d.ts
|
||||||
packages/app-desktop/gui/SearchBar/SearchBar.js
|
packages/app-desktop/gui/SearchBar/SearchBar.js
|
||||||
packages/app-desktop/gui/SearchBar/SearchBar.js.map
|
packages/app-desktop/gui/SearchBar/SearchBar.js.map
|
||||||
packages/app-desktop/gui/SearchBar/styles/index.d.ts
|
|
||||||
packages/app-desktop/gui/SearchBar/styles/index.js
|
|
||||||
packages/app-desktop/gui/SearchBar/styles/index.js.map
|
|
||||||
packages/app-desktop/gui/ShareNoteDialog.d.ts
|
packages/app-desktop/gui/ShareNoteDialog.d.ts
|
||||||
packages/app-desktop/gui/ShareNoteDialog.js
|
packages/app-desktop/gui/ShareNoteDialog.js
|
||||||
packages/app-desktop/gui/ShareNoteDialog.js.map
|
packages/app-desktop/gui/ShareNoteDialog.js.map
|
||||||
@ -707,6 +713,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/SearchInput/SearchInput.d.ts
|
||||||
|
packages/app-desktop/gui/lib/SearchInput/SearchInput.js
|
||||||
|
packages/app-desktop/gui/lib/SearchInput/SearchInput.js.map
|
||||||
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts
|
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
|
||||||
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
|
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
|
||||||
@ -1121,6 +1130,9 @@ packages/lib/services/plugins/Plugin.js.map
|
|||||||
packages/lib/services/plugins/PluginService.d.ts
|
packages/lib/services/plugins/PluginService.d.ts
|
||||||
packages/lib/services/plugins/PluginService.js
|
packages/lib/services/plugins/PluginService.js
|
||||||
packages/lib/services/plugins/PluginService.js.map
|
packages/lib/services/plugins/PluginService.js.map
|
||||||
|
packages/lib/services/plugins/RepositoryApi.d.ts
|
||||||
|
packages/lib/services/plugins/RepositoryApi.js
|
||||||
|
packages/lib/services/plugins/RepositoryApi.js.map
|
||||||
packages/lib/services/plugins/ToolbarButtonController.d.ts
|
packages/lib/services/plugins/ToolbarButtonController.d.ts
|
||||||
packages/lib/services/plugins/ToolbarButtonController.js
|
packages/lib/services/plugins/ToolbarButtonController.js
|
||||||
packages/lib/services/plugins/ToolbarButtonController.js.map
|
packages/lib/services/plugins/ToolbarButtonController.js.map
|
||||||
|
24
.gitignore
vendored
24
.gitignore
vendored
@ -315,9 +315,18 @@ 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/plugins/PluginBox.d.ts
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/PluginsStates.js.map
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js.map
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.d.ts
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js.map
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.d.ts
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js.map
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.d.ts
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/useOnInstallHandler.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
|
||||||
@ -645,9 +654,6 @@ packages/app-desktop/gui/Root_UpgradeSyncTarget.js.map
|
|||||||
packages/app-desktop/gui/SearchBar/SearchBar.d.ts
|
packages/app-desktop/gui/SearchBar/SearchBar.d.ts
|
||||||
packages/app-desktop/gui/SearchBar/SearchBar.js
|
packages/app-desktop/gui/SearchBar/SearchBar.js
|
||||||
packages/app-desktop/gui/SearchBar/SearchBar.js.map
|
packages/app-desktop/gui/SearchBar/SearchBar.js.map
|
||||||
packages/app-desktop/gui/SearchBar/styles/index.d.ts
|
|
||||||
packages/app-desktop/gui/SearchBar/styles/index.js
|
|
||||||
packages/app-desktop/gui/SearchBar/styles/index.js.map
|
|
||||||
packages/app-desktop/gui/ShareNoteDialog.d.ts
|
packages/app-desktop/gui/ShareNoteDialog.d.ts
|
||||||
packages/app-desktop/gui/ShareNoteDialog.js
|
packages/app-desktop/gui/ShareNoteDialog.js
|
||||||
packages/app-desktop/gui/ShareNoteDialog.js.map
|
packages/app-desktop/gui/ShareNoteDialog.js.map
|
||||||
@ -696,6 +702,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/SearchInput/SearchInput.d.ts
|
||||||
|
packages/app-desktop/gui/lib/SearchInput/SearchInput.js
|
||||||
|
packages/app-desktop/gui/lib/SearchInput/SearchInput.js.map
|
||||||
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.d.ts
|
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
|
||||||
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
|
packages/app-desktop/gui/lib/ToggleButton/ToggleButton.js.map
|
||||||
@ -1110,6 +1119,9 @@ packages/lib/services/plugins/Plugin.js.map
|
|||||||
packages/lib/services/plugins/PluginService.d.ts
|
packages/lib/services/plugins/PluginService.d.ts
|
||||||
packages/lib/services/plugins/PluginService.js
|
packages/lib/services/plugins/PluginService.js
|
||||||
packages/lib/services/plugins/PluginService.js.map
|
packages/lib/services/plugins/PluginService.js.map
|
||||||
|
packages/lib/services/plugins/RepositoryApi.d.ts
|
||||||
|
packages/lib/services/plugins/RepositoryApi.js
|
||||||
|
packages/lib/services/plugins/RepositoryApi.js.map
|
||||||
packages/lib/services/plugins/ToolbarButtonController.d.ts
|
packages/lib/services/plugins/ToolbarButtonController.d.ts
|
||||||
packages/lib/services/plugins/ToolbarButtonController.js
|
packages/lib/services/plugins/ToolbarButtonController.js
|
||||||
packages/lib/services/plugins/ToolbarButtonController.js.map
|
packages/lib/services/plugins/ToolbarButtonController.js.map
|
||||||
|
@ -5,7 +5,7 @@ import Button, { ButtonLevel } from '../Button/Button';
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import bridge from '../../services/bridge';
|
import bridge from '../../services/bridge';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import control_PluginsStates from './controls/PluginsStates';
|
import control_PluginsStates from './controls/plugins/PluginsStates';
|
||||||
|
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { themeStyle } = require('@joplin/lib/theme');
|
const { themeStyle } = require('@joplin/lib/theme');
|
||||||
@ -159,10 +159,14 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
const settingComps = createSettingComponents(false);
|
const settingComps = createSettingComponents(false);
|
||||||
const advancedSettingComps = createSettingComponents(true);
|
const advancedSettingComps = createSettingComponents(true);
|
||||||
|
|
||||||
|
const sectionWidths: Record<string, number> = {
|
||||||
|
plugins: 900,
|
||||||
|
};
|
||||||
|
|
||||||
const sectionStyle: any = {
|
const sectionStyle: any = {
|
||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
maxWidth: 640,
|
maxWidth: sectionWidths[section.name] ? sectionWidths[section.name] : 640,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!selected) sectionStyle.display = 'none';
|
if (!selected) sectionStyle.display = 'none';
|
||||||
@ -277,7 +281,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
color: theme.color,
|
color: theme.color,
|
||||||
fontSize: theme.fontSize * 1.083333,
|
fontSize: theme.fontSize * 1.083333,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
marginBottom: theme.mainPadding / 4,
|
marginBottom: theme.mainPadding / 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,7 +314,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
const output: any = null;
|
const output: any = null;
|
||||||
|
|
||||||
const rowStyle = {
|
const rowStyle = {
|
||||||
marginBottom: theme.mainPadding,
|
marginBottom: theme.mainPadding * 1.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const labelStyle = this.labelStyle(this.props.themeId);
|
const labelStyle = this.labelStyle(this.props.themeId);
|
||||||
@ -366,9 +370,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
|
|
||||||
if (settingKeyToControl[key]) {
|
if (settingKeyToControl[key]) {
|
||||||
const SettingComponent = settingKeyToControl[key];
|
const SettingComponent = settingKeyToControl[key];
|
||||||
|
const label = md.label ? this.renderLabel(this.props.themeId, md.label()) : null;
|
||||||
return (
|
return (
|
||||||
<div key={key} style={rowStyle}>
|
<div key={key} style={rowStyle}>
|
||||||
{this.renderLabel(this.props.themeId, md.label())}
|
{label}
|
||||||
{this.renderDescription(this.props.themeId, md.description ? md.description() : null)}
|
{this.renderDescription(this.props.themeId, md.description ? md.description() : null)}
|
||||||
<SettingComponent
|
<SettingComponent
|
||||||
metadata={md}
|
metadata={md}
|
||||||
@ -377,6 +382,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
updateSettingValue(key, event.value);
|
updateSettingValue(key, event.value);
|
||||||
}}
|
}}
|
||||||
|
renderLabel={this.renderLabel}
|
||||||
|
renderDescription={this.renderDescription}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,255 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,164 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import ToggleButton from '../../../lib/ToggleButton/ToggleButton';
|
||||||
|
import Button, { ButtonLevel } from '../../../Button/Button';
|
||||||
|
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||||
|
|
||||||
|
export enum InstallState {
|
||||||
|
NotInstalled = 1,
|
||||||
|
Installing = 2,
|
||||||
|
Installed = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
item?: PluginItem;
|
||||||
|
manifest?: PluginManifest;
|
||||||
|
installState?: InstallState;
|
||||||
|
themeId: number;
|
||||||
|
onToggle?: Function;
|
||||||
|
onDelete?: Function;
|
||||||
|
onInstall?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
function manifestToItem(manifest: PluginManifest): PluginItem {
|
||||||
|
return {
|
||||||
|
id: manifest.id,
|
||||||
|
name: manifest.name,
|
||||||
|
description: manifest.description,
|
||||||
|
enabled: true,
|
||||||
|
deleted: false,
|
||||||
|
devMode: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
enabled: boolean;
|
||||||
|
deleted: boolean;
|
||||||
|
devMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function(props: Props) {
|
||||||
|
const item = props.item ? props.item : manifestToItem(props.manifest);
|
||||||
|
|
||||||
|
// 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 (!props.onToggle) return null;
|
||||||
|
|
||||||
|
if (item.devMode) {
|
||||||
|
return <DevModeLabel>DEV</DevModeLabel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ToggleButton
|
||||||
|
themeId={props.themeId}
|
||||||
|
value={item.enabled}
|
||||||
|
onToggle={() => props.onToggle({ item })}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDeleteButton() {
|
||||||
|
if (!props.onDelete) return null;
|
||||||
|
return <Button level={ButtonLevel.Secondary} onClick={() => props.onDelete({ item })} title={_('Delete')}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInstallButton() {
|
||||||
|
if (!props.onInstall) return null;
|
||||||
|
|
||||||
|
let title = _('Install');
|
||||||
|
if (props.installState === InstallState.Installing) title = _('Installing...');
|
||||||
|
if (props.installState === InstallState.Installed) title = _('Installed');
|
||||||
|
|
||||||
|
return <Button
|
||||||
|
level={ButtonLevel.Secondary}
|
||||||
|
disabled={props.installState !== InstallState.NotInstalled}
|
||||||
|
onClick={() => props.onInstall({ item })}
|
||||||
|
title={title}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFooter() {
|
||||||
|
if (item.devMode) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CellFooter>
|
||||||
|
{renderDeleteButton()}
|
||||||
|
{renderInstallButton()}
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import PluginService, { defaultPluginSetting, Plugins, PluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import SearchPlugins from './SearchPlugins';
|
||||||
|
import PluginBox from './PluginBox';
|
||||||
|
// import Button, { ButtonLevel } from '../../../Button/Button';
|
||||||
|
import bridge from '../../../../services/bridge';
|
||||||
|
import produce from 'immer';
|
||||||
|
import { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
|
||||||
|
import { PluginItem } from './PluginBox';
|
||||||
|
const { space } = require('styled-system');
|
||||||
|
|
||||||
|
const Root = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserPluginsRoot = styled.div`
|
||||||
|
${space}
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// const InstallButton = styled(Button)``;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: any;
|
||||||
|
themeId: number;
|
||||||
|
onChange: Function;
|
||||||
|
renderLabel: Function;
|
||||||
|
renderDescription: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePluginItems(plugins: Plugins, settings: PluginSettings): PluginItem[] {
|
||||||
|
return useMemo(() => {
|
||||||
|
const output: PluginItem[] = [];
|
||||||
|
|
||||||
|
for (const pluginId in plugins) {
|
||||||
|
const plugin = plugins[pluginId];
|
||||||
|
|
||||||
|
const setting: PluginSetting = {
|
||||||
|
...defaultPluginSetting(),
|
||||||
|
...settings[pluginId],
|
||||||
|
};
|
||||||
|
|
||||||
|
output.push({
|
||||||
|
id: pluginId,
|
||||||
|
name: plugin.manifest.name,
|
||||||
|
description: plugin.manifest.description,
|
||||||
|
enabled: setting.enabled,
|
||||||
|
deleted: setting.deleted,
|
||||||
|
devMode: plugin.devMode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
output.sort((a: PluginItem, b: PluginItem) => {
|
||||||
|
return a.name < b.name ? -1 : +1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}, [plugins, settings]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(props: Props) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
const pluginService = PluginService.instance();
|
||||||
|
|
||||||
|
const pluginSettings = useMemo(() => {
|
||||||
|
return pluginService.unserializePluginSettings(props.value);
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
|
const onDelete = useCallback(async (event: any) => {
|
||||||
|
const item: PluginItem = event.item;
|
||||||
|
const confirm = await bridge().showConfirmMessageBox(_('Delete plugin "%s"?', item.name));
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
|
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||||
|
if (!draft[item.id]) draft[item.id] = defaultPluginSetting();
|
||||||
|
draft[item.id].deleted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
||||||
|
}, [pluginSettings, props.onChange]);
|
||||||
|
|
||||||
|
const onToggle = useCallback((event: any) => {
|
||||||
|
const item: PluginItem = event.item;
|
||||||
|
|
||||||
|
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||||
|
if (!draft[item.id]) draft[item.id] = defaultPluginSetting();
|
||||||
|
draft[item.id].enabled = !draft[item.id].enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
||||||
|
}, [pluginSettings, props.onChange]);
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
|
||||||
|
const onSearchQueryChange = useCallback((event: OnChangeEvent) => {
|
||||||
|
setSearchQuery(event.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSearchPluginSettingsChange = useCallback((event: any) => {
|
||||||
|
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
|
||||||
|
}, [props.onChange]);
|
||||||
|
|
||||||
|
function renderCells(items: PluginItem[]) {
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.deleted) continue;
|
||||||
|
|
||||||
|
output.push(<PluginBox
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
themeId={props.themeId}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onToggle={onToggle}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUserPlugins(pluginItems: PluginItem[]) {
|
||||||
|
const allDeleted = !pluginItems.find(it => it.deleted !== true);
|
||||||
|
|
||||||
|
if (!pluginItems.length || allDeleted) {
|
||||||
|
return (
|
||||||
|
<UserPluginsRoot mb={'10px'}>
|
||||||
|
{props.renderDescription(props.themeId, _('You do not have any installed plugin.'))}
|
||||||
|
</UserPluginsRoot>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<UserPluginsRoot>
|
||||||
|
{renderCells(pluginItems)}
|
||||||
|
</UserPluginsRoot>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInstallFromFile(): any {
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Disabled for now since there are already options for developments,
|
||||||
|
// and installing from file can be done by dropping the file in /plugins
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div>
|
||||||
|
// {props.renderLabel(props.themeId, _('Install plugin from file'))}
|
||||||
|
// <InstallButton level={ButtonLevel.Primary} onClick={onInstall} title={_('Install plugin')}/>
|
||||||
|
// <div style={{ display: 'flex', flex: 1 }}/>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<div style={{ marginBottom: 20 }}>
|
||||||
|
{props.renderLabel(props.themeId, _('Search for plugins'))}
|
||||||
|
<SearchPlugins
|
||||||
|
themeId={props.themeId}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
pluginSettings={pluginSettings}
|
||||||
|
onSearchQueryChange={onSearchQueryChange}
|
||||||
|
onPluginSettingsChange={onSearchPluginSettingsChange}
|
||||||
|
renderDescription={props.renderDescription}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{props.renderLabel(props.themeId, _('Manage your plugins'))}
|
||||||
|
{renderUserPlugins(pluginItems)}
|
||||||
|
|
||||||
|
{renderInstallFromFile()}
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import SearchInput, { OnChangeEvent } from '../../../lib/SearchInput/SearchInput';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
||||||
|
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||||
|
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||||
|
import PluginBox, { InstallState } from './PluginBox';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import useOnInstallHandler from './useOnInstallHandler';
|
||||||
|
|
||||||
|
const Root = styled.div`
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ResultsRoot = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
searchQuery: string;
|
||||||
|
onSearchQueryChange(event: OnChangeEvent): void;
|
||||||
|
pluginSettings: PluginSettings;
|
||||||
|
onPluginSettingsChange(event: any): void;
|
||||||
|
renderDescription: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
let repoApi_: RepositoryApi = null;
|
||||||
|
|
||||||
|
function repoApi(): RepositoryApi {
|
||||||
|
if (repoApi_) return repoApi_;
|
||||||
|
repoApi_ = new RepositoryApi('https://github.com/joplin/plugins', Setting.value('tempDir'));
|
||||||
|
return repoApi_;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(props: Props) {
|
||||||
|
const [searchStarted, setSearchStarted] = useState(false);
|
||||||
|
const [manifests, setManifests] = useState<PluginManifest[]>([]);
|
||||||
|
const asyncSearchQueue = useRef(new AsyncActionQueue(200));
|
||||||
|
const [installingPluginsIds, setInstallingPluginIds] = useState<Record<string, boolean>>({});
|
||||||
|
const [searchResultCount, setSearchResultCount] = useState(null);
|
||||||
|
|
||||||
|
const onInstall = useOnInstallHandler(setInstallingPluginIds, props.pluginSettings, repoApi, props.onPluginSettingsChange);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchResultCount(null);
|
||||||
|
asyncSearchQueue.current.push(async () => {
|
||||||
|
if (!props.searchQuery) {
|
||||||
|
setManifests([]);
|
||||||
|
setSearchResultCount(null);
|
||||||
|
} else {
|
||||||
|
const r = await repoApi().search(props.searchQuery);
|
||||||
|
setManifests(r);
|
||||||
|
setSearchResultCount(r.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [props.searchQuery]);
|
||||||
|
|
||||||
|
const onChange = useCallback((event: OnChangeEvent) => {
|
||||||
|
setSearchStarted(true);
|
||||||
|
props.onSearchQueryChange(event);
|
||||||
|
}, [props.onSearchQueryChange]);
|
||||||
|
|
||||||
|
const onSearchButtonClick = useCallback(() => {
|
||||||
|
setSearchStarted(false);
|
||||||
|
props.onSearchQueryChange({ value: '' });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function installState(pluginId: string): InstallState {
|
||||||
|
const settings = props.pluginSettings[pluginId];
|
||||||
|
if (settings && !settings.deleted) return InstallState.Installed;
|
||||||
|
if (installingPluginsIds[pluginId]) return InstallState.Installing;
|
||||||
|
return InstallState.NotInstalled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(query: string, manifests: PluginManifest[]) {
|
||||||
|
if (query && !manifests.length) {
|
||||||
|
if (searchResultCount === null) return ''; // Search in progress
|
||||||
|
return props.renderDescription(props.themeId, _('No results'));
|
||||||
|
} else {
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
for (const manifest of manifests) {
|
||||||
|
output.push(<PluginBox
|
||||||
|
key={manifest.id}
|
||||||
|
manifest={manifest}
|
||||||
|
themeId={props.themeId}
|
||||||
|
onInstall={onInstall}
|
||||||
|
installState={installState(manifest.id)}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<div style={{ marginBottom: 10, width: 250 }}>
|
||||||
|
<SearchInput
|
||||||
|
inputRef={null}
|
||||||
|
value={props.searchQuery}
|
||||||
|
onChange={onChange}
|
||||||
|
onSearchButtonClick={onSearchButtonClick}
|
||||||
|
searchStarted={searchStarted}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ResultsRoot>
|
||||||
|
{renderResults(props.searchQuery, manifests)}
|
||||||
|
</ResultsRoot>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import PluginService, { defaultPluginSetting, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||||
|
import produce from 'immer';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import Logger from '@joplin/lib/Logger';
|
||||||
|
|
||||||
|
const logger = Logger.create('useOnInstallHandler');
|
||||||
|
|
||||||
|
export default function(setInstallingPluginIds: Function, pluginSettings: PluginSettings, repoApi: Function, onPluginSettingsChange: Function) {
|
||||||
|
return useCallback(async (event: any) => {
|
||||||
|
const pluginId = event.item.id;
|
||||||
|
|
||||||
|
setInstallingPluginIds((prev: any) => {
|
||||||
|
return {
|
||||||
|
...prev, [pluginId]: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let installError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PluginService.instance().installPluginFromRepo(repoApi(), pluginId);
|
||||||
|
} catch (error) {
|
||||||
|
installError = error;
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!installError) {
|
||||||
|
const newSettings = produce(pluginSettings, (draft: PluginSettings) => {
|
||||||
|
draft[pluginId] = defaultPluginSetting();
|
||||||
|
});
|
||||||
|
|
||||||
|
onPluginSettingsChange({ value: newSettings });
|
||||||
|
}
|
||||||
|
|
||||||
|
setInstallingPluginIds((prev: any) => {
|
||||||
|
return {
|
||||||
|
...prev, [pluginId]: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (installError) alert(_('Could not install plugin: %s', installError.message));
|
||||||
|
}, [pluginSettings, onPluginSettingsChange]);
|
||||||
|
}
|
@ -1,16 +1,20 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import SearchInput from '../lib/SearchInput/SearchInput';
|
||||||
import { Root, SearchInput, SearchButton, SearchButtonIcon } from './styles';
|
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
import { _ } from '@joplin/lib/locale';
|
|
||||||
import { stateUtils } from '@joplin/lib/reducer';
|
import { stateUtils } from '@joplin/lib/reducer';
|
||||||
import BaseModel from '@joplin/lib/BaseModel';
|
import BaseModel from '@joplin/lib/BaseModel';
|
||||||
import uuid from '@joplin/lib/uuid';
|
import uuid from '@joplin/lib/uuid';
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const Note = require('@joplin/lib/models/Note');
|
const Note = require('@joplin/lib/models/Note');
|
||||||
const debounce = require('debounce');
|
const debounce = require('debounce');
|
||||||
|
const styled = require('styled-components').default;
|
||||||
|
|
||||||
|
export const Root = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
inputRef?: any;
|
inputRef?: any;
|
||||||
@ -22,7 +26,6 @@ interface Props {
|
|||||||
function SearchBar(props: Props) {
|
function SearchBar(props: Props) {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [searchStarted, setSearchStarted] = useState(false);
|
const [searchStarted, setSearchStarted] = useState(false);
|
||||||
const iconName = !searchStarted ? CommandService.instance().iconName('search') : 'fa fa-times';
|
|
||||||
const searchId = useRef(uuid.create());
|
const searchId = useRef(uuid.create());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -78,7 +81,7 @@ function SearchBar(props: Props) {
|
|||||||
|
|
||||||
function onChange(event: any) {
|
function onChange(event: any) {
|
||||||
setSearchStarted(true);
|
setSearchStarted(true);
|
||||||
setQuery(event.currentTarget.value);
|
setQuery(event.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFocus() {
|
function onFocus() {
|
||||||
@ -120,19 +123,15 @@ function SearchBar(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<Root>
|
<Root>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
ref={props.inputRef}
|
inputRef={props.inputRef}
|
||||||
value={query}
|
value={query}
|
||||||
type="text"
|
|
||||||
placeholder={_('Search...')}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
spellCheck={false}
|
onSearchButtonClick={onSearchButtonClick}
|
||||||
|
searchStarted={searchStarted}
|
||||||
/>
|
/>
|
||||||
<SearchButton onClick={onSearchButtonClick}>
|
|
||||||
<SearchButtonIcon className={iconName}/>
|
|
||||||
</SearchButton>
|
|
||||||
</Root>
|
</Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import StyledInput from '../../style/StyledInput';
|
|
||||||
const styled = require('styled-components').default;
|
|
||||||
|
|
||||||
export const Root = styled.div`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SearchButton = styled.button`
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
height: 100%;
|
|
||||||
opacity: ${(props: any) => props.disabled ? 0.5 : 1};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SearchButtonIcon = styled.span`
|
|
||||||
font-size: ${(props: any) => props.theme.toolbarIconSize}px;
|
|
||||||
color: ${(props: any) => props.theme.color4};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SearchInput = styled(StyledInput)`
|
|
||||||
padding-right: 20px;
|
|
||||||
flex: 1;
|
|
||||||
width: 10px;
|
|
||||||
`;
|
|
75
packages/app-desktop/gui/lib/SearchInput/SearchInput.tsx
Normal file
75
packages/app-desktop/gui/lib/SearchInput/SearchInput.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
|
import StyledInput from '../../style/StyledInput';
|
||||||
|
const styled = require('styled-components').default;
|
||||||
|
|
||||||
|
export const Root = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SearchButton = styled.button`
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
height: 100%;
|
||||||
|
opacity: ${(props: any) => props.disabled ? 0.5 : 1};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SearchButtonIcon = styled.span`
|
||||||
|
font-size: ${(props: any) => props.theme.toolbarIconSize}px;
|
||||||
|
color: ${(props: any) => props.theme.color4};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SearchInput = styled(StyledInput)`
|
||||||
|
padding-right: 20px;
|
||||||
|
flex: 1;
|
||||||
|
width: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
inputRef?: any;
|
||||||
|
value: string;
|
||||||
|
onChange(event: OnChangeEvent): void;
|
||||||
|
onFocus?: Function;
|
||||||
|
onBlur?: Function;
|
||||||
|
onKeyDown?: Function;
|
||||||
|
onSearchButtonClick: Function;
|
||||||
|
searchStarted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OnChangeEvent {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(props: Props) {
|
||||||
|
const iconName = !props.searchStarted ? CommandService.instance().iconName('search') : 'fa fa-times';
|
||||||
|
|
||||||
|
const onChange = useCallback((event: any) => {
|
||||||
|
props.onChange({ value: event.currentTarget.value });
|
||||||
|
}, [props.onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<SearchInput
|
||||||
|
ref={props.inputRef}
|
||||||
|
value={props.value}
|
||||||
|
type="text"
|
||||||
|
placeholder={_('Search...')}
|
||||||
|
onChange={onChange}
|
||||||
|
onFocus={props.onFocus}
|
||||||
|
onBlur={props.onBlur}
|
||||||
|
onKeyDown={props.onKeyDown}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
<SearchButton onClick={props.onSearchButtonClick}>
|
||||||
|
<SearchButtonIcon className={iconName}/>
|
||||||
|
</SearchButton>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
@ -20,19 +20,18 @@ export enum IntervalType {
|
|||||||
// Each queue should be associated with a specific entity (a note, resource, etc.)
|
// Each queue should be associated with a specific entity (a note, resource, etc.)
|
||||||
export default class AsyncActionQueue {
|
export default class AsyncActionQueue {
|
||||||
|
|
||||||
queue_: QueueItem[] = [];
|
private queue_: QueueItem[] = [];
|
||||||
interval_: number;
|
private interval_: number;
|
||||||
intervalType_: number;
|
private intervalType_: number;
|
||||||
scheduleProcessingIID_: any = null;
|
private scheduleProcessingIID_: any = null;
|
||||||
processing_ = false;
|
private processing_ = false;
|
||||||
needProcessing_ = false;
|
|
||||||
|
|
||||||
constructor(interval: number = 100, intervalType: IntervalType = IntervalType.Debounce) {
|
public constructor(interval: number = 100, intervalType: IntervalType = IntervalType.Debounce) {
|
||||||
this.interval_ = interval;
|
this.interval_ = interval;
|
||||||
this.intervalType_ = intervalType;
|
this.intervalType_ = intervalType;
|
||||||
}
|
}
|
||||||
|
|
||||||
push(action: QueueItemAction, context: any = null) {
|
public push(action: QueueItemAction, context: any = null) {
|
||||||
this.queue_.push({
|
this.queue_.push({
|
||||||
action: action,
|
action: action,
|
||||||
context: context,
|
context: context,
|
||||||
@ -40,8 +39,8 @@ export default class AsyncActionQueue {
|
|||||||
this.scheduleProcessing();
|
this.scheduleProcessing();
|
||||||
}
|
}
|
||||||
|
|
||||||
get queue(): QueueItem[] {
|
public get isEmpty(): boolean {
|
||||||
return this.queue_;
|
return !this.queue_.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private scheduleProcessing(interval: number = null) {
|
private scheduleProcessing(interval: number = null) {
|
||||||
@ -77,7 +76,7 @@ export default class AsyncActionQueue {
|
|||||||
this.processing_ = false;
|
this.processing_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async reset() {
|
public async reset() {
|
||||||
if (this.scheduleProcessingIID_) {
|
if (this.scheduleProcessingIID_) {
|
||||||
shim.clearTimeout(this.scheduleProcessingIID_);
|
shim.clearTimeout(this.scheduleProcessingIID_);
|
||||||
this.scheduleProcessingIID_ = null;
|
this.scheduleProcessingIID_ = null;
|
||||||
@ -89,11 +88,11 @@ export default class AsyncActionQueue {
|
|||||||
|
|
||||||
// Currently waitForAllDone() already finishes all the actions
|
// Currently waitForAllDone() already finishes all the actions
|
||||||
// as quickly as possible so we can make it an alias.
|
// as quickly as possible so we can make it an alias.
|
||||||
async processAllNow() {
|
public async processAllNow() {
|
||||||
return this.waitForAllDone();
|
return this.waitForAllDone();
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForAllDone() {
|
public async waitForAllDone() {
|
||||||
if (!this.queue_.length) return Promise.resolve();
|
if (!this.queue_.length) return Promise.resolve();
|
||||||
|
|
||||||
this.scheduleProcessing(1);
|
this.scheduleProcessing(1);
|
||||||
|
@ -619,7 +619,6 @@ class Setting extends BaseModel {
|
|||||||
section: 'plugins',
|
section: 'plugins',
|
||||||
public: true,
|
public: true,
|
||||||
appTypes: ['desktop'],
|
appTypes: ['desktop'],
|
||||||
label: () => _('Plugins'),
|
|
||||||
needRestart: true,
|
needRestart: true,
|
||||||
autoSave: true,
|
autoSave: true,
|
||||||
},
|
},
|
||||||
@ -629,6 +628,7 @@ class Setting extends BaseModel {
|
|||||||
type: SettingItemType.String,
|
type: SettingItemType.String,
|
||||||
section: 'plugins',
|
section: 'plugins',
|
||||||
public: true,
|
public: true,
|
||||||
|
advanced: true,
|
||||||
appTypes: ['desktop'],
|
appTypes: ['desktop'],
|
||||||
label: () => 'Development plugins',
|
label: () => 'Development plugins',
|
||||||
description: () => 'You may add multiple plugin paths, each separated by a comma. You will need to restart the application for the changes to take effect.',
|
description: () => 'You may add multiple plugin paths, each separated by a comma. You will need to restart the application for the changes to take effect.',
|
||||||
@ -1504,7 +1504,7 @@ class Setting extends BaseModel {
|
|||||||
if (name === 'appearance') return _('Appearance');
|
if (name === 'appearance') return _('Appearance');
|
||||||
if (name === 'note') return _('Note');
|
if (name === 'note') return _('Note');
|
||||||
if (name === 'markdownPlugins') return _('Markdown');
|
if (name === 'markdownPlugins') return _('Markdown');
|
||||||
if (name === 'plugins') return `${_('Plugins')} (Beta)`;
|
if (name === 'plugins') return _('Plugins');
|
||||||
if (name === 'application') return _('Application');
|
if (name === 'application') return _('Application');
|
||||||
if (name === 'revisionService') return _('Note History');
|
if (name === 'revisionService') return _('Note History');
|
||||||
if (name === 'encryption') return _('Encryption');
|
if (name === 'encryption') return _('Encryption');
|
||||||
|
@ -7,6 +7,7 @@ import shim from '../../shim';
|
|||||||
import { filename, dirname, rtrimSlashes } from '../../path-utils';
|
import { filename, dirname, rtrimSlashes } from '../../path-utils';
|
||||||
import Setting from '../../models/Setting';
|
import Setting from '../../models/Setting';
|
||||||
import Logger from '../../Logger';
|
import Logger from '../../Logger';
|
||||||
|
import RepositoryApi from './RepositoryApi';
|
||||||
const compareVersions = require('compare-versions');
|
const compareVersions = require('compare-versions');
|
||||||
const uslug = require('uslug');
|
const uslug = require('uslug');
|
||||||
const md5File = require('md5-file/promise');
|
const md5File = require('md5-file/promise');
|
||||||
@ -328,6 +329,13 @@ export default class PluginService extends BaseService {
|
|||||||
return this.runner_.run(plugin, pluginApi);
|
return this.runner_.run(plugin, pluginApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async installPluginFromRepo(repoApi: RepositoryApi, pluginId: string): Promise<Plugin> {
|
||||||
|
const pluginPath = await repoApi.downloadPlugin(pluginId);
|
||||||
|
const plugin = await this.installPlugin(pluginPath);
|
||||||
|
await shim.fsDriver().remove(pluginPath);
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
public async installPlugin(jplPath: string): Promise<Plugin> {
|
public async installPlugin(jplPath: string): Promise<Plugin> {
|
||||||
logger.info(`Installing plugin: "${jplPath}"`);
|
logger.info(`Installing plugin: "${jplPath}"`);
|
||||||
|
|
||||||
|
86
packages/lib/services/plugins/RepositoryApi.ts
Normal file
86
packages/lib/services/plugins/RepositoryApi.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import shim from '../../shim';
|
||||||
|
import { PluginManifest } from './utils/types';
|
||||||
|
const md5 = require('md5');
|
||||||
|
|
||||||
|
export default class RepositoryApi {
|
||||||
|
|
||||||
|
// For now, it's assumed that the baseUrl is a GitHub repo URL, such as
|
||||||
|
// https://github.com/joplin/plugins
|
||||||
|
//
|
||||||
|
// Later on, other repo types could be supported.
|
||||||
|
private baseUrl_: string;
|
||||||
|
private tempDir_: string;
|
||||||
|
private manifests_: PluginManifest[] = null;
|
||||||
|
|
||||||
|
public constructor(baseUrl: string, tempDir: string) {
|
||||||
|
this.baseUrl_ = baseUrl;
|
||||||
|
this.tempDir_ = tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get contentBaseUrl(): string {
|
||||||
|
return `${this.baseUrl_.replace(/github\.com/, 'raw.githubusercontent.com')}/master`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fileUrl(relativePath: string): string {
|
||||||
|
return `${this.contentBaseUrl}/${relativePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchText(path: string): Promise<string> {
|
||||||
|
return shim.fetchText(this.fileUrl(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async search(query: string): Promise<PluginManifest[]> {
|
||||||
|
query = query.toLowerCase().trim();
|
||||||
|
|
||||||
|
const manifests = await this.manifests();
|
||||||
|
const output: PluginManifest[] = [];
|
||||||
|
|
||||||
|
for (const manifest of manifests) {
|
||||||
|
for (const field of ['name', 'description']) {
|
||||||
|
const v = (manifest as any)[field];
|
||||||
|
if (!v) continue;
|
||||||
|
|
||||||
|
if (v.toLowerCase().indexOf(query) >= 0) {
|
||||||
|
output.push(manifest);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a temporary path, where the plugin has been downloaded to. Temp
|
||||||
|
// file should be deleted by caller.
|
||||||
|
public async downloadPlugin(pluginId: string): Promise<string> {
|
||||||
|
const manifests = await this.manifests();
|
||||||
|
const manifest = manifests.find(m => m.id === pluginId);
|
||||||
|
if (!manifest) throw new Error(`No manifest for plugin ID "${pluginId}"`);
|
||||||
|
|
||||||
|
const fileUrl = this.fileUrl(`plugins/${manifest.id}/plugin.jpl`);
|
||||||
|
const hash = md5(Date.now() + Math.random());
|
||||||
|
const targetPath = `${this.tempDir_}/${hash}_${manifest.id}.jpl`;
|
||||||
|
const response = await shim.fetchBlob(fileUrl, {
|
||||||
|
path: targetPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error(`Could not download plugin "${pluginId}" from "${fileUrl}"`);
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async manifests(): Promise<PluginManifest[]> {
|
||||||
|
if (this.manifests_) return this.manifests_;
|
||||||
|
const manifestsText = await this.fetchText('manifests.json');
|
||||||
|
try {
|
||||||
|
const manifests = JSON.parse(manifestsText);
|
||||||
|
if (!manifests) throw new Error('Invalid or missing JSON');
|
||||||
|
this.manifests_ = Object.keys(manifests).map(id => {
|
||||||
|
return manifests[id];
|
||||||
|
});
|
||||||
|
return this.manifests_;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Could not parse JSON: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -16,4 +16,7 @@ export interface PluginManifest {
|
|||||||
|
|
||||||
// Private keys
|
// Private keys
|
||||||
_package_hash?: string;
|
_package_hash?: string;
|
||||||
|
_publish_hash?: string;
|
||||||
|
_publish_commit?: string;
|
||||||
|
_npm_package_name?: string;
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,12 @@ const shim = {
|
|||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fetchText: async (url: string, options: any = null): Promise<string> => {
|
||||||
|
const r = await shim.fetch(url, options || {});
|
||||||
|
if (!r.ok) throw new Error(`Could not fetch ${url}`);
|
||||||
|
return r.text();
|
||||||
|
},
|
||||||
|
|
||||||
createResourceFromPath: async (_filePath: string, _defaultProps: any = null, _options: any = null): Promise<ResourceEntity> => {
|
createResourceFromPath: async (_filePath: string, _defaultProps: any = null, _options: any = null): Promise<ResourceEntity> => {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
},
|
},
|
||||||
@ -209,7 +215,7 @@ const shim = {
|
|||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchBlob: async function(_url: string, _options: any = null) {
|
fetchBlob: function(_url: string, _options: any = null): any {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user