mirror of https://github.com/laurent22/joplin.git
Mobile: Add support for plugin panels and dialogs (#10121)
parent
b9eb4522f5
commit
b3ec92a57e
|
@ -480,6 +480,11 @@ packages/app-desktop/utils/markupLanguageUtils.js
|
||||||
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||||
packages/app-mobile/PluginAssetsLoader.js
|
packages/app-mobile/PluginAssetsLoader.js
|
||||||
|
packages/app-mobile/commands/index.js
|
||||||
|
packages/app-mobile/commands/openItem.js
|
||||||
|
packages/app-mobile/commands/openNote.js
|
||||||
|
packages/app-mobile/commands/scrollToHash.js
|
||||||
|
packages/app-mobile/commands/util/goToNote.js
|
||||||
packages/app-mobile/components/ActionButton.js
|
packages/app-mobile/components/ActionButton.js
|
||||||
packages/app-mobile/components/BackButtonDialogBox.js
|
packages/app-mobile/components/BackButtonDialogBox.js
|
||||||
packages/app-mobile/components/CameraView.js
|
packages/app-mobile/components/CameraView.js
|
||||||
|
@ -585,12 +590,21 @@ packages/app-mobile/gulpfile.js
|
||||||
packages/app-mobile/plugins/PlatformImplementation.js
|
packages/app-mobile/plugins/PlatformImplementation.js
|
||||||
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
||||||
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||||
packages/app-mobile/plugins/PluginRunner/types.js
|
packages/app-mobile/plugins/PluginRunner/types.js
|
||||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||||
|
|
|
@ -460,6 +460,11 @@ packages/app-desktop/utils/markupLanguageUtils.js
|
||||||
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
packages/app-desktop/utils/restartInSafeModeFromMain.test.js
|
||||||
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
packages/app-desktop/utils/restartInSafeModeFromMain.js
|
||||||
packages/app-mobile/PluginAssetsLoader.js
|
packages/app-mobile/PluginAssetsLoader.js
|
||||||
|
packages/app-mobile/commands/index.js
|
||||||
|
packages/app-mobile/commands/openItem.js
|
||||||
|
packages/app-mobile/commands/openNote.js
|
||||||
|
packages/app-mobile/commands/scrollToHash.js
|
||||||
|
packages/app-mobile/commands/util/goToNote.js
|
||||||
packages/app-mobile/components/ActionButton.js
|
packages/app-mobile/components/ActionButton.js
|
||||||
packages/app-mobile/components/BackButtonDialogBox.js
|
packages/app-mobile/components/BackButtonDialogBox.js
|
||||||
packages/app-mobile/components/CameraView.js
|
packages/app-mobile/components/CameraView.js
|
||||||
|
@ -565,12 +570,21 @@ packages/app-mobile/gulpfile.js
|
||||||
packages/app-mobile/plugins/PlatformImplementation.js
|
packages/app-mobile/plugins/PlatformImplementation.js
|
||||||
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
|
||||||
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
|
||||||
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
|
||||||
|
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
|
||||||
packages/app-mobile/plugins/PluginRunner/types.js
|
packages/app-mobile/plugins/PluginRunner/types.js
|
||||||
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
|
||||||
packages/app-mobile/plugins/hooks/usePlugin.js
|
packages/app-mobile/plugins/hooks/usePlugin.js
|
||||||
|
|
|
@ -243,7 +243,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||||
|
|
||||||
if (commands[cmd.name]) {
|
if (commands[cmd.name]) {
|
||||||
commandOutput = commands[cmd.name](cmd.value);
|
commandOutput = commands[cmd.name](cmd.value);
|
||||||
} else if (editorRef.current.supportsCommand(cmd)) {
|
} else if (await editorRef.current.supportsCommand(cmd)) {
|
||||||
commandOutput = editorRef.current.execCommandFromJoplin(cmd);
|
commandOutput = editorRef.current.execCommandFromJoplin(cmd);
|
||||||
} else {
|
} else {
|
||||||
reg.logger().warn('CodeMirror: unsupported Joplin command: ', cmd);
|
reg.logger().warn('CodeMirror: unsupported Joplin command: ', cmd);
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const declaration: CommandDeclaration = {
|
||||||
export const runtime = (comp: any): CommandRuntime => {
|
export const runtime = (comp: any): CommandRuntime => {
|
||||||
return {
|
return {
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
if (comp.editorRef.current && comp.editorRef.current.supportsCommand('search')) {
|
if (comp.editorRef.current && await comp.editorRef.current.supportsCommand('search')) {
|
||||||
comp.editorRef.current.execCommand({ name: 'search' });
|
comp.editorRef.current.execCommand({ name: 'search' });
|
||||||
} else {
|
} else {
|
||||||
if (comp.noteSearchBarRef.current) {
|
if (comp.noteSearchBarRef.current) {
|
||||||
|
|
|
@ -57,7 +57,7 @@ export interface NoteBodyEditorRef {
|
||||||
resetScroll(): void;
|
resetScroll(): void;
|
||||||
scrollTo(options: ScrollOptions): void;
|
scrollTo(options: ScrollOptions): void;
|
||||||
|
|
||||||
supportsCommand(name: string): boolean;
|
supportsCommand(name: string): boolean|Promise<boolean>;
|
||||||
execCommand(command: CommandValue): Promise<void>;
|
execCommand(command: CommandValue): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
||||||
|
import * as openItem from './openItem';
|
||||||
|
import * as openNote from './openNote';
|
||||||
|
import * as scrollToHash from './scrollToHash';
|
||||||
|
|
||||||
|
const index: any[] = [
|
||||||
|
openItem,
|
||||||
|
openNote,
|
||||||
|
scrollToHash,
|
||||||
|
];
|
||||||
|
|
||||||
|
export default index;
|
||||||
|
// AUTO-GENERATED using `gulp buildScriptIndexes`
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
const { parseResourceUrl, urlProtocol } = require('@joplin/lib/urlUtils');
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import goToNote from './util/goToNote';
|
||||||
|
|
||||||
|
const logger = Logger.create('openItemCommand');
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'openItem',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (_context: CommandContext, link: string) => {
|
||||||
|
if (!link) throw new Error('Link cannot be empty');
|
||||||
|
|
||||||
|
if (link.startsWith('joplin://') || link.startsWith(':/')) {
|
||||||
|
const parsedUrl = parseResourceUrl(link);
|
||||||
|
if (parsedUrl) {
|
||||||
|
const { itemId, hash } = parsedUrl;
|
||||||
|
|
||||||
|
logger.info(`Navigating to item ${itemId}`);
|
||||||
|
await goToNote(itemId, hash);
|
||||||
|
} else {
|
||||||
|
logger.error(`Invalid Joplin link: ${link}`);
|
||||||
|
}
|
||||||
|
} else if (urlProtocol(link)) {
|
||||||
|
shim.openUrl(link);
|
||||||
|
} else {
|
||||||
|
const errorMessage = _('Unsupported link or message: %s', link);
|
||||||
|
logger.error(errorMessage);
|
||||||
|
await shim.showMessageBox(errorMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import goToNote from './util/goToNote';
|
||||||
|
|
||||||
|
const logger = Logger.create('openNoteCommand');
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'openNote',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (_context: CommandContext, noteId: string, hash: string = null) => {
|
||||||
|
logger.info(`Navigating to note ${noteId}`);
|
||||||
|
await goToNote(noteId, hash);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||||
|
import NavService from '@joplin/lib/services/NavService';
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'scrollToHash',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (context: CommandContext, hash: string) => {
|
||||||
|
const selectedNoteIds = context.state.selectedNoteIds;
|
||||||
|
if (selectedNoteIds.length === 0) {
|
||||||
|
throw new Error('Unable to scroll to hash -- no note open.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await NavService.go('Note', {
|
||||||
|
noteId: selectedNoteIds[0],
|
||||||
|
noteHash: hash,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Note from '@joplin/lib/models/Note';
|
||||||
|
import NavService from '@joplin/lib/services/NavService';
|
||||||
|
|
||||||
|
const goToNote = async (id: string, hash?: string) => {
|
||||||
|
if (!(await Note.load(id))) {
|
||||||
|
throw new Error(`No note with id ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NavService.go('Note', {
|
||||||
|
noteId: id,
|
||||||
|
noteHash: hash,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default goToNote;
|
|
@ -18,7 +18,6 @@ import { _ } from '@joplin/lib/locale';
|
||||||
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
|
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
|
||||||
import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events';
|
||||||
import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from '@joplin/editor/types';
|
import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from '@joplin/editor/types';
|
||||||
import supportsCommand from '@joplin/editor/CodeMirror/editorCommands/supportsCommand';
|
|
||||||
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
|
import SelectionFormatting, { defaultSelectionFormatting } from '@joplin/editor/SelectionFormatting';
|
||||||
import useCodeMirrorPlugins from './hooks/useCodeMirrorPlugins';
|
import useCodeMirrorPlugins from './hooks/useCodeMirrorPlugins';
|
||||||
import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
|
import RNToWebViewMessenger from '../../utils/ipc/RNToWebViewMessenger';
|
||||||
|
@ -159,7 +158,7 @@ const useEditorControl = (
|
||||||
|
|
||||||
const control: EditorControl = {
|
const control: EditorControl = {
|
||||||
supportsCommand(command: EditorCommandType) {
|
supportsCommand(command: EditorCommandType) {
|
||||||
return supportsCommand(command);
|
return bodyControl.supportsCommand(command);
|
||||||
},
|
},
|
||||||
execCommand(command, ...args: any[]) {
|
execCommand(command, ...args: any[]) {
|
||||||
return bodyControl.execCommand(command, ...args);
|
return bodyControl.execCommand(command, ...args);
|
||||||
|
|
|
@ -17,7 +17,7 @@ const commandRuntime = (declaration: CommandDeclaration, editor: EditorControl)
|
||||||
args = args.slice(1);
|
args = args.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!editor.supportsCommand(commandName)) {
|
if (!(await editor.supportsCommand(commandName))) {
|
||||||
logger.warn('Command not supported by editor: ', commandName);
|
logger.warn('Command not supported by editor: ', commandName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,8 +58,10 @@ import { SelectionRange } from '../NoteEditor/types';
|
||||||
import { AppState } from '../../utils/types';
|
import { AppState } from '../../utils/types';
|
||||||
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||||
import { getDisplayParentTitle } from '@joplin/lib/services/trash';
|
import { getDisplayParentTitle } from '@joplin/lib/services/trash';
|
||||||
import pickDocument from '../../utils/pickDocument';
|
|
||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
|
import pickDocument from '../../utils/pickDocument';
|
||||||
|
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||||
|
import PluginPanelViewer from '../../plugins/PluginRunner/dialogs/PluginPanelViewer';
|
||||||
const urlUtils = require('@joplin/lib/urlUtils');
|
const urlUtils = require('@joplin/lib/urlUtils');
|
||||||
|
|
||||||
const emptyArray: any[] = [];
|
const emptyArray: any[] = [];
|
||||||
|
@ -101,6 +103,7 @@ interface State {
|
||||||
imageEditorResourceFilepath: string;
|
imageEditorResourceFilepath: string;
|
||||||
noteResources: Record<string, ResourceEntity>;
|
noteResources: Record<string, ResourceEntity>;
|
||||||
newAndNoTitleChangeNoteId: boolean|null;
|
newAndNoTitleChangeNoteId: boolean|null;
|
||||||
|
pluginPanelsVisible: boolean;
|
||||||
|
|
||||||
HACK_webviewLoadingState: number;
|
HACK_webviewLoadingState: number;
|
||||||
|
|
||||||
|
@ -159,6 +162,7 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||||
noteResources: {},
|
noteResources: {},
|
||||||
imageEditorResourceFilepath: null,
|
imageEditorResourceFilepath: null,
|
||||||
newAndNoTitleChangeNoteId: null,
|
newAndNoTitleChangeNoteId: null,
|
||||||
|
pluginPanelsVisible: false,
|
||||||
|
|
||||||
// HACK: For reasons I can't explain, when the WebView is present, the TextInput initially does not display (It's just a white rectangle with
|
// HACK: For reasons I can't explain, when the WebView is present, the TextInput initially does not display (It's just a white rectangle with
|
||||||
// no visible text). It will only appear when tapping it or doing certain action like selecting text on the webview. The bug started to
|
// no visible text). It will only appear when tapping it or doing certain action like selecting text on the webview. The bug started to
|
||||||
|
@ -258,21 +262,12 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||||
if (!item) throw new Error(_('No item with ID %s', itemId));
|
if (!item) throw new Error(_('No item with ID %s', itemId));
|
||||||
|
|
||||||
if (item.type_ === BaseModel.TYPE_NOTE) {
|
if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||||
// Easier to just go back, then go to the note since
|
|
||||||
// the Note screen doesn't handle reloading a different note
|
|
||||||
|
|
||||||
this.props.dispatch({
|
this.props.dispatch({
|
||||||
type: 'NAV_BACK',
|
type: 'NAV_GO',
|
||||||
|
routeName: 'Note',
|
||||||
|
noteId: item.id,
|
||||||
|
noteHash: resourceUrlInfo.hash,
|
||||||
});
|
});
|
||||||
|
|
||||||
shim.setTimeout(() => {
|
|
||||||
this.props.dispatch({
|
|
||||||
type: 'NAV_GO',
|
|
||||||
routeName: 'Note',
|
|
||||||
noteId: item.id,
|
|
||||||
noteHash: resourceUrlInfo.hash,
|
|
||||||
});
|
|
||||||
}, 5);
|
|
||||||
} else if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
} else if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
||||||
if (!(await Resource.isReady(item))) throw new Error(_('This attachment is not downloaded or not decrypted yet.'));
|
if (!(await Resource.isReady(item))) throw new Error(_('This attachment is not downloaded or not decrypted yet.'));
|
||||||
|
|
||||||
|
@ -556,6 +551,28 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||||
disableSideMenuGestures: this.state.showImageEditor,
|
disableSideMenuGestures: this.state.showImageEditor,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.noteId && prevProps.noteId !== this.props.noteId) {
|
||||||
|
// Easier to just go back, then go to the note since
|
||||||
|
// the Note screen doesn't handle reloading a different note
|
||||||
|
const noteId = this.props.noteId;
|
||||||
|
const noteHash = this.props.noteHash;
|
||||||
|
|
||||||
|
this.props.dispatch({
|
||||||
|
type: 'NAV_GO',
|
||||||
|
routeName: 'Notes',
|
||||||
|
folderId: this.state.note.parent_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
shim.setTimeout(() => {
|
||||||
|
this.props.dispatch({
|
||||||
|
type: 'NAV_GO',
|
||||||
|
routeName: 'Note',
|
||||||
|
noteId: noteId,
|
||||||
|
noteHash: noteHash,
|
||||||
|
});
|
||||||
|
}, 5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
@ -1283,6 +1300,18 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||||
disabled: readOnly,
|
disabled: readOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Only show the plugin panel toggle if any plugins have panels
|
||||||
|
const allPluginViews = Object.values(this.props.plugins).map(plugin => Object.values(plugin.views)).flat();
|
||||||
|
const allPanels = allPluginViews.filter(view => view.containerType === ContainerType.Panel);
|
||||||
|
if (allPanels.length > 0) {
|
||||||
|
output.push({
|
||||||
|
title: _('Show plugin panels'),
|
||||||
|
onPress: () => {
|
||||||
|
this.setState({ pluginPanelsVisible: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.menuOptionsCache_ = {};
|
this.menuOptionsCache_ = {};
|
||||||
this.menuOptionsCache_[cacheKey] = output;
|
this.menuOptionsCache_[cacheKey] = output;
|
||||||
|
|
||||||
|
@ -1594,6 +1623,10 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{noteTagDialog}
|
{noteTagDialog}
|
||||||
|
<PluginPanelViewer
|
||||||
|
visible={this.state.pluginPanelsVisible}
|
||||||
|
onClose={() => this.setState({ pluginPanelsVisible: false })}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
// https://github.com/facebook/metro/issues/1#issuecomment-511228599
|
// https://github.com/facebook/metro/issues/1#issuecomment-511228599
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const { getDefaultConfig } = require('metro-config');
|
||||||
|
|
||||||
const localPackages = {
|
const localPackages = {
|
||||||
'@joplin/lib': path.resolve(__dirname, '../lib/'),
|
'@joplin/lib': path.resolve(__dirname, '../lib/'),
|
||||||
|
@ -45,6 +46,8 @@ for (const [, v] of Object.entries(localPackages)) {
|
||||||
watchedFolders.push(v);
|
watchedFolders.push(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultConfig = getDefaultConfig.getDefaultValues(__dirname);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
transformer: {
|
transformer: {
|
||||||
getTransformOptions: async () => ({
|
getTransformOptions: async () => ({
|
||||||
|
@ -55,6 +58,13 @@ module.exports = {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
resolver: {
|
resolver: {
|
||||||
|
assetExts: [
|
||||||
|
...defaultConfig.resolver.assetExts,
|
||||||
|
|
||||||
|
// Allow loading .jpl plugin files
|
||||||
|
'jpl',
|
||||||
|
],
|
||||||
|
|
||||||
// This configuration allows you to build React-Native modules and test
|
// This configuration allows you to build React-Native modules and test
|
||||||
// them without having to publish the module. Any exports provided by
|
// them without having to publish the module. Any exports provided by
|
||||||
// your source should be added to the "target" parameter. Any import not
|
// your source should be added to the "target" parameter. Any import not
|
||||||
|
|
|
@ -117,6 +117,7 @@
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"typescript": "5.2.2",
|
"typescript": "5.2.2",
|
||||||
"uglify-js": "3.17.4",
|
"uglify-js": "3.17.4",
|
||||||
|
"punycode": "2.3.1",
|
||||||
"webpack": "5.74.0"
|
"webpack": "5.74.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,14 @@ import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
import PluginRunner from './PluginRunner';
|
import PluginRunner from './PluginRunner';
|
||||||
import loadPlugins from '../loadPlugins';
|
import loadPlugins from '../loadPlugins';
|
||||||
import { useStore } from 'react-redux';
|
import { connect, useStore } from 'react-redux';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginHtmlContents, PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||||
|
import PluginDialogManager from './dialogs/PluginDialogManager';
|
||||||
|
import { AppState } from '../../utils/types';
|
||||||
|
|
||||||
const logger = Logger.create('PluginRunnerWebView');
|
const logger = Logger.create('PluginRunnerWebView');
|
||||||
|
|
||||||
|
@ -41,9 +43,11 @@ const usePlugins = (
|
||||||
interface Props {
|
interface Props {
|
||||||
serializedPluginSettings: string;
|
serializedPluginSettings: string;
|
||||||
pluginStates: PluginStates;
|
pluginStates: PluginStates;
|
||||||
|
pluginHtmlContents: PluginHtmlContents;
|
||||||
|
themeId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PluginRunnerWebView: React.FC<Props> = props => {
|
const PluginRunnerWebViewComponent: React.FC<Props> = props => {
|
||||||
const webviewRef = useRef<WebViewControl>();
|
const webviewRef = useRef<WebViewControl>();
|
||||||
|
|
||||||
const [webviewLoaded, setLoaded] = useState(false);
|
const [webviewLoaded, setLoaded] = useState(false);
|
||||||
|
@ -105,15 +109,22 @@ const PluginRunnerWebView: React.FC<Props> = props => {
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExtendedWebView
|
<>
|
||||||
webviewInstanceId='PluginRunner'
|
<ExtendedWebView
|
||||||
html={html}
|
webviewInstanceId='PluginRunner'
|
||||||
injectedJavaScript={injectedJs}
|
html={html}
|
||||||
onMessage={pluginRunner.onWebviewMessage}
|
injectedJavaScript={injectedJs}
|
||||||
onLoadEnd={onLoadEnd}
|
onMessage={pluginRunner.onWebviewMessage}
|
||||||
onLoadStart={onLoadStart}
|
onLoadEnd={onLoadEnd}
|
||||||
ref={webviewRef}
|
onLoadStart={onLoadStart}
|
||||||
/>
|
ref={webviewRef}
|
||||||
|
/>
|
||||||
|
<PluginDialogManager
|
||||||
|
themeId={props.themeId}
|
||||||
|
pluginHtmlContents={props.pluginHtmlContents}
|
||||||
|
pluginStates={props.pluginStates}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,4 +135,12 @@ const PluginRunnerWebView: React.FC<Props> = props => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PluginRunnerWebView;
|
export default connect((state: AppState) => {
|
||||||
|
const result: Props = {
|
||||||
|
serializedPluginSettings: state.settings['plugins.states'],
|
||||||
|
pluginStates: state.pluginService.plugins,
|
||||||
|
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||||
|
themeId: state.settings.theme,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
})(PluginRunnerWebViewComponent);
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { DialogWebViewApi, DialogMainProcessApi } from '../types';
|
||||||
|
import reportUnhandledErrors from './utils/reportUnhandledErrors';
|
||||||
|
import wrapConsoleLog from './utils/wrapConsoleLog';
|
||||||
|
import WebViewToRNMessenger from '../../../utils/ipc/WebViewToRNMessenger';
|
||||||
|
|
||||||
|
let themeCssElement: HTMLStyleElement|null = null;
|
||||||
|
|
||||||
|
const initializeDialogWebView = (messageChannelId: string) => {
|
||||||
|
const loadedPaths: Set<string> = new Set();
|
||||||
|
|
||||||
|
type ScriptType = 'js'|'css';
|
||||||
|
const includeScriptsOrStyles = (type: ScriptType, paths: string[]) => {
|
||||||
|
for (const path of paths) {
|
||||||
|
if (loadedPaths.has(path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
loadedPaths.add(path);
|
||||||
|
|
||||||
|
if (type === 'css') {
|
||||||
|
const stylesheetLink = document.createElement('link');
|
||||||
|
stylesheetLink.rel = 'stylesheet';
|
||||||
|
stylesheetLink.href = path;
|
||||||
|
document.head.appendChild(stylesheetLink);
|
||||||
|
} else {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = path;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const localApi: DialogWebViewApi = {
|
||||||
|
includeCssFiles: async (paths: string[]) => {
|
||||||
|
return includeScriptsOrStyles('css', paths);
|
||||||
|
},
|
||||||
|
includeJsFiles: async (paths: string[]) => {
|
||||||
|
return includeScriptsOrStyles('js', paths);
|
||||||
|
},
|
||||||
|
getFormData: async () => {
|
||||||
|
const firstForm = document.querySelector('form');
|
||||||
|
if (!firstForm) return null;
|
||||||
|
|
||||||
|
const formData = new FormData(firstForm);
|
||||||
|
|
||||||
|
const result = Object.create(null);
|
||||||
|
for (const key of formData.keys()) {
|
||||||
|
result[key] = formData.get(key);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
setThemeCss: async (css: string) => {
|
||||||
|
themeCssElement?.remove?.();
|
||||||
|
const styleElement = document.createElement('style');
|
||||||
|
styleElement.appendChild(document.createTextNode(css));
|
||||||
|
document.body.appendChild(styleElement);
|
||||||
|
themeCssElement = styleElement;
|
||||||
|
},
|
||||||
|
getContentSize: async () => {
|
||||||
|
// To convert to React Native pixel units from browser pixel units,
|
||||||
|
// we need to multiply by the devicePixelRatio:
|
||||||
|
const dpr = window.devicePixelRatio ?? 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
width: document.body.clientWidth * dpr,
|
||||||
|
height: document.body.clientHeight * dpr,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const messenger = new WebViewToRNMessenger<DialogWebViewApi, DialogMainProcessApi>(messageChannelId, localApi);
|
||||||
|
|
||||||
|
(window as any).webviewApi = {
|
||||||
|
postMessage: messenger.remoteApi.postMessage,
|
||||||
|
onMessage: messenger.remoteApi.onMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
reportUnhandledErrors(messenger.remoteApi.onError);
|
||||||
|
wrapConsoleLog(messenger.remoteApi.onLog);
|
||||||
|
|
||||||
|
// If dialog content scripts were bundled with Webpack for NodeJS,
|
||||||
|
// they may expect a global "exports" to be present.
|
||||||
|
(window as any).exports ??= {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initializeDialogWebView;
|
|
@ -1,12 +1,45 @@
|
||||||
export { runPlugin, stopPlugin } from './startStopPlugin';
|
export { runPlugin, stopPlugin } from './startStopPlugin';
|
||||||
|
|
||||||
|
// Old plugins allowed to import legacy APIs
|
||||||
|
const legacyPluginIds = [
|
||||||
|
'outline',
|
||||||
|
'ylc395.noteLinkSystem',
|
||||||
|
];
|
||||||
|
|
||||||
const pathLibrary = require('path');
|
const pathLibrary = require('path');
|
||||||
export const requireModule = (moduleName: string) => {
|
const punycode = require('punycode/');
|
||||||
|
|
||||||
|
export const requireModule = (moduleName: string, fromPluginId: string) => {
|
||||||
if (moduleName === 'path') {
|
if (moduleName === 'path') {
|
||||||
return pathLibrary;
|
return pathLibrary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (legacyPluginIds.includes(fromPluginId)) {
|
||||||
|
if (moduleName === 'punycode') {
|
||||||
|
console.warn('Requiring punycode is deprecated. Please transition to a newer API.');
|
||||||
|
return punycode;
|
||||||
|
}
|
||||||
|
if (moduleName === 'fs' || moduleName === 'fs-extra') {
|
||||||
|
console.warn('The fs library is unavailable to mobile plugins. A non-functional mock will be returned.');
|
||||||
|
return {
|
||||||
|
existsSync: () => false,
|
||||||
|
pathExists: () => false,
|
||||||
|
readFileSync: () => '',
|
||||||
|
readFile: () => '',
|
||||||
|
writeFileSync: () => '',
|
||||||
|
writeFile: () => '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (moduleName === 'process') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (moduleName === 'url') {
|
||||||
|
return { parse: (u: string) => new URL(u) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(`Unable to require module ${moduleName} on mobile.`);
|
throw new Error(`Unable to require module ${moduleName} on mobile.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { default as initializePluginBackgroundIframe } from './initializePluginBackgroundIframe';
|
export { default as initializePluginBackgroundIframe } from './initializePluginBackgroundIframe';
|
||||||
|
export { default as initializeDialogWebView } from './initializeDialogWebView';
|
||||||
|
|
|
@ -40,7 +40,9 @@ export const runPlugin = (
|
||||||
${pluginBackgroundScript}
|
${pluginBackgroundScript}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
window.require = pluginBackgroundPage.requireModule;
|
window.require = function(module) {
|
||||||
|
return pluginBackgroundPage.requireModule(module, ${JSON.stringify(pluginId)});
|
||||||
|
};
|
||||||
window.exports = window.exports || {};
|
window.exports = window.exports || {};
|
||||||
|
|
||||||
await pluginBackgroundPage.initializePluginBackgroundIframe(${JSON.stringify(messageChannelId)});
|
await pluginBackgroundPage.initializePluginBackgroundIframe(${JSON.stringify(messageChannelId)});
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ReactElement } from 'react';
|
||||||
|
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||||
|
import PluginDialogWebView from './PluginDialogWebView';
|
||||||
|
import { Modal, Portal } from 'react-native-paper';
|
||||||
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
|
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||||
|
import useViewInfos from './hooks/useViewInfos';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
|
||||||
|
pluginHtmlContents: PluginHtmlContents;
|
||||||
|
pluginStates: PluginStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dismissDialog = (viewInfo: ViewInfo) => {
|
||||||
|
if (!viewInfo.view.opened) return;
|
||||||
|
|
||||||
|
const plugin = PluginService.instance().pluginById(viewInfo.plugin.id);
|
||||||
|
const viewController = plugin.viewController(viewInfo.view.id) as WebviewController;
|
||||||
|
viewController.closeWithResponse(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PluginDialogManager: React.FC<Props> = props => {
|
||||||
|
const viewInfos = useViewInfos(props.pluginStates);
|
||||||
|
|
||||||
|
const dialogs: ReactElement[] = [];
|
||||||
|
for (const viewInfo of viewInfos) {
|
||||||
|
if (viewInfo.view.containerType === ContainerType.Panel || !viewInfo.view.opened) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogs.push(
|
||||||
|
<Portal
|
||||||
|
key={`${viewInfo.plugin.id}-${viewInfo.view.id}`}
|
||||||
|
>
|
||||||
|
<Modal
|
||||||
|
visible={true}
|
||||||
|
onDismiss={() => dismissDialog(viewInfo)}
|
||||||
|
>
|
||||||
|
<PluginDialogWebView
|
||||||
|
viewInfo={viewInfo}
|
||||||
|
themeId={props.themeId}
|
||||||
|
pluginStates={props.pluginStates}
|
||||||
|
pluginHtmlContents={props.pluginHtmlContents}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</Portal>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{dialogs}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginDialogManager;
|
|
@ -0,0 +1,156 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||||
|
import { StyleSheet, View, useWindowDimensions } from 'react-native';
|
||||||
|
import usePlugin from '../../hooks/usePlugin';
|
||||||
|
import { DialogContentSize, DialogWebViewApi } from '../types';
|
||||||
|
import { Button } from 'react-native-paper';
|
||||||
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
|
import { ButtonSpec, DialogResult } from '@joplin/lib/services/plugins/api/types';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||||
|
import { Theme } from '@joplin/lib/themes/type';
|
||||||
|
import useDialogSize from './hooks/useDialogSize';
|
||||||
|
import PluginUserWebView from './PluginUserWebView';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
pluginHtmlContents: PluginHtmlContents;
|
||||||
|
pluginStates: PluginStates;
|
||||||
|
viewInfo: ViewInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = (
|
||||||
|
themeId: number,
|
||||||
|
dialogContentSize: DialogContentSize|null,
|
||||||
|
fitToContent: boolean,
|
||||||
|
) => {
|
||||||
|
const windowSize = useWindowDimensions();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const theme: Theme = themeStyle(themeId);
|
||||||
|
|
||||||
|
const useDialogSize = fitToContent && dialogContentSize;
|
||||||
|
const dialogHasLoaded = !!dialogContentSize;
|
||||||
|
|
||||||
|
return StyleSheet.create({
|
||||||
|
webView: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
webViewContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
backgroundColor: theme.backgroundColor,
|
||||||
|
borderRadius: 12,
|
||||||
|
|
||||||
|
maxHeight: useDialogSize ? dialogContentSize?.height : undefined,
|
||||||
|
maxWidth: useDialogSize ? dialogContentSize?.width : undefined,
|
||||||
|
height: windowSize.height * 0.97,
|
||||||
|
width: windowSize.width * 0.97,
|
||||||
|
opacity: dialogHasLoaded ? 1 : 0.1,
|
||||||
|
|
||||||
|
// Center
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: 12,
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [themeId, dialogContentSize, fitToContent, windowSize.width, windowSize.height]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultButtonSpecs: ButtonSpec[] = [
|
||||||
|
{ id: 'ok' }, { id: 'cancel' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PluginDialogWebView: React.FC<Props> = props => {
|
||||||
|
const viewInfo = props.viewInfo;
|
||||||
|
const view = viewInfo.view;
|
||||||
|
const pluginId = viewInfo.plugin.id;
|
||||||
|
const viewId = view.id;
|
||||||
|
const plugin = usePlugin(pluginId);
|
||||||
|
const [webViewLoadCount, setWebViewLoadCount] = useState(0);
|
||||||
|
const [dialogControl, setDialogControl] = useState<DialogWebViewApi|null>(null);
|
||||||
|
|
||||||
|
const dialogSize = useDialogSize({
|
||||||
|
dialogControl,
|
||||||
|
webViewLoadCount,
|
||||||
|
});
|
||||||
|
const styles = useStyles(props.themeId, dialogSize, view.fitToContent);
|
||||||
|
|
||||||
|
const onButtonPress = useCallback(async (button: ButtonSpec) => {
|
||||||
|
const closeWithResponse = (response?: DialogResult|null) => {
|
||||||
|
const viewController = plugin.viewController(viewId) as WebviewController;
|
||||||
|
if (view.containerType === ContainerType.Dialog) {
|
||||||
|
viewController.closeWithResponse(response);
|
||||||
|
} else {
|
||||||
|
viewController.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let formData = undefined;
|
||||||
|
if (button.id !== 'cancel') {
|
||||||
|
formData = await dialogControl.getFormData();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeWithResponse({ id: button.id, formData });
|
||||||
|
button.onClick?.();
|
||||||
|
}, [dialogControl, plugin, viewId, view.containerType]);
|
||||||
|
|
||||||
|
const buttonComponents: React.ReactElement[] = [];
|
||||||
|
const buttonSpecs = view.buttons ?? defaultButtonSpecs;
|
||||||
|
|
||||||
|
for (const button of buttonSpecs) {
|
||||||
|
let iconName = undefined;
|
||||||
|
let buttonTitle = button.title ?? button.id;
|
||||||
|
|
||||||
|
if (button.id === 'cancel') {
|
||||||
|
iconName = 'close-outline';
|
||||||
|
buttonTitle = button.title ?? _('Cancel');
|
||||||
|
} else if (button.id === 'ok') {
|
||||||
|
iconName = 'check';
|
||||||
|
buttonTitle = button.title ?? _('OK');
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonComponents.push(
|
||||||
|
<Button
|
||||||
|
key={button.id}
|
||||||
|
icon={iconName}
|
||||||
|
mode='text'
|
||||||
|
onPress={() => onButtonPress(button)}
|
||||||
|
>
|
||||||
|
{buttonTitle}
|
||||||
|
</Button>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWebViewLoaded = useCallback(() => {
|
||||||
|
setWebViewLoadCount(webViewLoadCount + 1);
|
||||||
|
}, [setWebViewLoadCount, webViewLoadCount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.dialog}>
|
||||||
|
<View style={styles.webViewContainer}>
|
||||||
|
<PluginUserWebView
|
||||||
|
style={styles.webView}
|
||||||
|
themeId={props.themeId}
|
||||||
|
viewInfo={props.viewInfo}
|
||||||
|
pluginHtmlContents={props.pluginHtmlContents}
|
||||||
|
onLoadEnd={onWebViewLoaded}
|
||||||
|
setDialogControl={setDialogControl}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.buttonRow}>
|
||||||
|
{buttonComponents}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginDialogWebView;
|
|
@ -0,0 +1,184 @@
|
||||||
|
|
||||||
|
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Button, IconButton, Modal, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
||||||
|
import useViewInfos from './hooks/useViewInfos';
|
||||||
|
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { AppState } from '../../../utils/types';
|
||||||
|
import PluginUserWebView from './PluginUserWebView';
|
||||||
|
import { View, useWindowDimensions, StyleSheet } from 'react-native';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { Theme } from '@joplin/lib/themes/type';
|
||||||
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
|
||||||
|
pluginHtmlContents: PluginHtmlContents;
|
||||||
|
pluginStates: PluginStates;
|
||||||
|
visible: boolean;
|
||||||
|
onClose: ()=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const useStyles = (themeId: number) => {
|
||||||
|
const windowSize = useWindowDimensions();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const theme: Theme = themeStyle(themeId);
|
||||||
|
|
||||||
|
return StyleSheet.create({
|
||||||
|
webView: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
webViewContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
|
closeButtonContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
backgroundColor: theme.backgroundColor,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 10,
|
||||||
|
|
||||||
|
height: windowSize.height * 0.9,
|
||||||
|
width: windowSize.width * 0.97,
|
||||||
|
|
||||||
|
// Center
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [themeId, windowSize.width, windowSize.height]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyCallback = () => {};
|
||||||
|
|
||||||
|
const PluginPanelViewer: React.FC<Props> = props => {
|
||||||
|
const viewInfos = useViewInfos(props.pluginStates);
|
||||||
|
const viewInfoById = useMemo(() => {
|
||||||
|
const result: Record<string, ViewInfo> = {};
|
||||||
|
for (const info of viewInfos) {
|
||||||
|
result[`${info.plugin.id}--${info.view.id}`] = info;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [viewInfos]);
|
||||||
|
|
||||||
|
const buttonInfos = useMemo(() => {
|
||||||
|
return Object.entries(viewInfoById)
|
||||||
|
.filter(([_id, info]) => info.view.containerType === ContainerType.Panel)
|
||||||
|
.map(([id, info]) => {
|
||||||
|
const pluginName = PluginService.instance().pluginById(info.plugin.id).manifest.name;
|
||||||
|
return {
|
||||||
|
value: id,
|
||||||
|
label: pluginName,
|
||||||
|
icon: 'puzzle',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [viewInfoById]);
|
||||||
|
|
||||||
|
const [selectedTabId, setSelectedTabId] = useState(() => {
|
||||||
|
const lastSelectedId = Setting.value('ui.lastSelectedPluginPanel');
|
||||||
|
if (lastSelectedId && viewInfoById[lastSelectedId]) {
|
||||||
|
return lastSelectedId;
|
||||||
|
} else {
|
||||||
|
return buttonInfos[0]?.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedTabId) return () => {};
|
||||||
|
|
||||||
|
const info = viewInfoById[selectedTabId];
|
||||||
|
const plugin = PluginService.instance().pluginById(info.plugin.id);
|
||||||
|
const controller = plugin.viewController(info.view.id) as WebviewController;
|
||||||
|
controller.setIsShownInModal(true);
|
||||||
|
Setting.setValue('ui.lastSelectedPluginPanel', selectedTabId);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
controller.setIsShownInModal(false);
|
||||||
|
};
|
||||||
|
}, [viewInfoById, selectedTabId]);
|
||||||
|
|
||||||
|
|
||||||
|
const styles = useStyles(props.themeId);
|
||||||
|
|
||||||
|
const viewInfo = viewInfoById[selectedTabId];
|
||||||
|
|
||||||
|
const renderTabContent = () => {
|
||||||
|
if (!viewInfo) {
|
||||||
|
return <Text>{_('No tab selected')}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.webViewContainer}>
|
||||||
|
<PluginUserWebView
|
||||||
|
key={selectedTabId}
|
||||||
|
themeId={props.themeId}
|
||||||
|
style={styles.webView}
|
||||||
|
viewInfo={viewInfo}
|
||||||
|
pluginHtmlContents={props.pluginHtmlContents}
|
||||||
|
onLoadEnd={emptyCallback}
|
||||||
|
setDialogControl={emptyCallback}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTabSelector = () => {
|
||||||
|
// SegmentedButtons doesn't display correctly when there's only one button.
|
||||||
|
// As such, we include a special case:
|
||||||
|
if (buttonInfos.length === 1) {
|
||||||
|
const buttonInfo = buttonInfos[0];
|
||||||
|
return <Button icon={buttonInfo.icon}>{buttonInfo.label}</Button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SegmentedButtons
|
||||||
|
value={selectedTabId}
|
||||||
|
onValueChange={setSelectedTabId}
|
||||||
|
buttons={buttonInfos}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeButton = (
|
||||||
|
<View style={styles.closeButtonContainer}>
|
||||||
|
<IconButton
|
||||||
|
icon='close'
|
||||||
|
accessibilityLabel={_('Close')}
|
||||||
|
onPress={props.onClose}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Modal
|
||||||
|
visible={props.visible}
|
||||||
|
onDismiss={props.onClose}
|
||||||
|
contentContainerStyle={styles.dialog}
|
||||||
|
>
|
||||||
|
{closeButton}
|
||||||
|
{renderTabContent()}
|
||||||
|
{renderTabSelector()}
|
||||||
|
</Modal>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect((state: AppState) => {
|
||||||
|
return {
|
||||||
|
themeId: state.settings.theme,
|
||||||
|
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||||
|
pluginStates: state.pluginService.plugins,
|
||||||
|
};
|
||||||
|
})(PluginPanelViewer);
|
|
@ -0,0 +1,114 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { PluginHtmlContents, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||||
|
import ExtendedWebView, { WebViewControl } from '../../../components/ExtendedWebView';
|
||||||
|
import { ViewStyle } from 'react-native';
|
||||||
|
import usePlugin from '../../hooks/usePlugin';
|
||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
import useDialogMessenger from './hooks/useDialogMessenger';
|
||||||
|
import useWebViewSetup from './hooks/useWebViewSetup';
|
||||||
|
import { DialogWebViewApi } from '../types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
pluginHtmlContents: PluginHtmlContents;
|
||||||
|
viewInfo: ViewInfo;
|
||||||
|
style: ViewStyle;
|
||||||
|
onLoadEnd: ()=> void;
|
||||||
|
setDialogControl: (dialogControl: DialogWebViewApi)=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginUserWebView = (props: Props) => {
|
||||||
|
const viewInfo = props.viewInfo;
|
||||||
|
const view = viewInfo.view;
|
||||||
|
const pluginId = viewInfo.plugin.id;
|
||||||
|
const viewId = view.id;
|
||||||
|
const plugin = usePlugin(pluginId);
|
||||||
|
const [webViewLoadCount, setWebViewLoadCount] = useState(0);
|
||||||
|
|
||||||
|
const webviewRef = useRef<WebViewControl>(null);
|
||||||
|
|
||||||
|
const messageChannelId = `dialog-messenger-${pluginId}-${viewId}`;
|
||||||
|
const messenger = useDialogMessenger({
|
||||||
|
pluginId,
|
||||||
|
viewId,
|
||||||
|
webviewRef,
|
||||||
|
messageChannelId,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Because of how messenger.remoteApi handles message forwarding (property names
|
||||||
|
// are not known), we need to send methods individually and can't use an object
|
||||||
|
// spread or send messenger.remoteApi.
|
||||||
|
props.setDialogControl({
|
||||||
|
includeCssFiles: messenger.remoteApi.includeCssFiles,
|
||||||
|
includeJsFiles: messenger.remoteApi.includeJsFiles,
|
||||||
|
setThemeCss: messenger.remoteApi.setThemeCss,
|
||||||
|
getFormData: messenger.remoteApi.getFormData,
|
||||||
|
getContentSize: messenger.remoteApi.getContentSize,
|
||||||
|
});
|
||||||
|
}, [messenger, props.setDialogControl]);
|
||||||
|
|
||||||
|
useWebViewSetup({
|
||||||
|
themeId: props.themeId,
|
||||||
|
dialogControl: messenger.remoteApi,
|
||||||
|
scriptPaths: view.scripts ?? [],
|
||||||
|
pluginBaseDir: plugin.baseDir,
|
||||||
|
webViewLoadCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const htmlContent = props.pluginHtmlContents[pluginId]?.[viewId] ?? '';
|
||||||
|
const html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||||
|
<title>Plugin Dialog</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--joplin-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${htmlContent}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const injectedJs = useMemo(() => {
|
||||||
|
return `
|
||||||
|
if (!window.backgroundPageLoaded) {
|
||||||
|
${shim.injectedJs('pluginBackgroundPage')}
|
||||||
|
pluginBackgroundPage.initializeDialogWebView(
|
||||||
|
${JSON.stringify(messageChannelId)}
|
||||||
|
);
|
||||||
|
|
||||||
|
window.backgroundPageLoaded = true;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}, [messageChannelId]);
|
||||||
|
|
||||||
|
const onWebViewLoaded = useCallback(() => {
|
||||||
|
setWebViewLoadCount(webViewLoadCount + 1);
|
||||||
|
props.onLoadEnd();
|
||||||
|
messenger.onWebViewLoaded();
|
||||||
|
}, [messenger, setWebViewLoadCount, webViewLoadCount, props.onLoadEnd]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ExtendedWebView
|
||||||
|
style={props.style}
|
||||||
|
baseUrl={plugin.baseDir}
|
||||||
|
webviewInstanceId='joplin__PluginDialogWebView'
|
||||||
|
html={html}
|
||||||
|
injectedJavaScript={injectedJs}
|
||||||
|
onMessage={messenger.onWebViewMessage}
|
||||||
|
onLoadEnd={onWebViewLoaded}
|
||||||
|
ref={webviewRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginUserWebView;
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { useMemo, RefObject } from 'react';
|
||||||
|
import { DialogMainProcessApi, DialogWebViewApi } from '../../types';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import { WebViewControl } from '../../../../components/ExtendedWebView';
|
||||||
|
import createOnLogHander from '../../utils/createOnLogHandler';
|
||||||
|
import RNToWebViewMessenger from '../../../../utils/ipc/RNToWebViewMessenger';
|
||||||
|
import { SerializableData } from '@joplin/lib/utils/ipc/types';
|
||||||
|
import PostMessageService, { ResponderComponentType } from '@joplin/lib/services/PostMessageService';
|
||||||
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pluginId: string;
|
||||||
|
viewId: string;
|
||||||
|
webviewRef: RefObject<WebViewControl>;
|
||||||
|
messageChannelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useDialogMessenger = (props: Props) => {
|
||||||
|
const { pluginId, webviewRef, viewId, messageChannelId } = props;
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const plugin = PluginService.instance().pluginById(pluginId);
|
||||||
|
const logger = Logger.create(`PluginDialogWebView(${pluginId})`);
|
||||||
|
|
||||||
|
const dialogApi: DialogMainProcessApi = {
|
||||||
|
postMessage: async (message: SerializableData) => {
|
||||||
|
return await plugin.viewController(viewId).emitMessage({ message });
|
||||||
|
},
|
||||||
|
onMessage: async (callback) => {
|
||||||
|
PostMessageService.instance().registerViewMessageHandler(
|
||||||
|
ResponderComponentType.UserWebview,
|
||||||
|
viewId,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: async (error: string) => {
|
||||||
|
logger.error(`Unhandled error: ${error}`);
|
||||||
|
plugin.hasErrors = true;
|
||||||
|
},
|
||||||
|
onLog: createOnLogHander(plugin, logger),
|
||||||
|
};
|
||||||
|
|
||||||
|
return new RNToWebViewMessenger<DialogMainProcessApi, DialogWebViewApi>(
|
||||||
|
messageChannelId, webviewRef, dialogApi,
|
||||||
|
);
|
||||||
|
}, [webviewRef, pluginId, viewId, messageChannelId]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDialogMessenger;
|
|
@ -0,0 +1,29 @@
|
||||||
|
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { DialogContentSize, DialogWebViewApi } from '../../types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dialogControl: DialogWebViewApi;
|
||||||
|
webViewLoadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useDialogSize = (props: Props) => {
|
||||||
|
const { dialogControl, webViewLoadCount } = props;
|
||||||
|
|
||||||
|
const [dialogSize, setDialogSize] = useState<DialogContentSize|null>(null);
|
||||||
|
useAsyncEffect(async event => {
|
||||||
|
if (!dialogControl) {
|
||||||
|
// May happen if the webview is still loading.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentSize = await dialogControl.getContentSize();
|
||||||
|
if (event.cancelled) return;
|
||||||
|
|
||||||
|
setDialogSize(contentSize);
|
||||||
|
}, [dialogControl, setDialogSize, webViewLoadCount]);
|
||||||
|
|
||||||
|
return dialogSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDialogSize;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
const useViewInfos = (pluginStates: PluginStates) => {
|
||||||
|
return useMemo(() => {
|
||||||
|
return pluginUtils.viewInfosByType(pluginStates, 'webview');
|
||||||
|
}, [pluginStates]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useViewInfos;
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { DialogWebViewApi } from '../../types';
|
||||||
|
import shim from '@joplin/lib/shim';
|
||||||
|
import { themeStyle } from '../../../../components/global-style';
|
||||||
|
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
scriptPaths: string[];
|
||||||
|
dialogControl: DialogWebViewApi;
|
||||||
|
pluginBaseDir: string;
|
||||||
|
|
||||||
|
// Whenever the WebView reloads, we need to re-inject CSS and JavaScript.
|
||||||
|
webViewLoadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useWebViewSetup = (props: Props) => {
|
||||||
|
const { scriptPaths, dialogControl, pluginBaseDir, themeId } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const jsPaths = [];
|
||||||
|
const cssPaths = [];
|
||||||
|
for (const rawPath of scriptPaths) {
|
||||||
|
const resolvedPath = shim.fsDriver().resolveRelativePathWithinDir(pluginBaseDir, rawPath);
|
||||||
|
|
||||||
|
if (resolvedPath.match(/\.css$/i)) {
|
||||||
|
cssPaths.push(resolvedPath);
|
||||||
|
} else {
|
||||||
|
jsPaths.push(resolvedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void dialogControl.includeCssFiles(cssPaths);
|
||||||
|
void dialogControl.includeJsFiles(jsPaths);
|
||||||
|
}, [dialogControl, scriptPaths, props.webViewLoadCount, pluginBaseDir]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const theme = themeStyle(themeId);
|
||||||
|
const themeVariableCss = themeToCss(theme);
|
||||||
|
void dialogControl.setThemeCss(themeVariableCss);
|
||||||
|
}, [dialogControl, themeId, props.webViewLoadCount]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWebViewSetup;
|
|
@ -1160,10 +1160,7 @@ class AppComponent extends React.Component {
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</MenuProvider>
|
</MenuProvider>
|
||||||
</SideMenu>
|
</SideMenu>
|
||||||
<PluginRunnerWebView
|
<PluginRunnerWebView />
|
||||||
serializedPluginSettings={this.props.serializedPluginSettings}
|
|
||||||
pluginStates={this.props.pluginStates}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1217,8 +1214,6 @@ const mapStateToProps = (state: any) => {
|
||||||
disableSideMenuGestures: state.disableSideMenuGestures,
|
disableSideMenuGestures: state.disableSideMenuGestures,
|
||||||
biometricsDone: state.biometricsDone,
|
biometricsDone: state.biometricsDone,
|
||||||
biometricsEnabled: state.settings['security.biometricsEnabled'],
|
biometricsEnabled: state.settings['security.biometricsEnabled'],
|
||||||
serializedPluginSettings: state.settings['plugins.states'],
|
|
||||||
pluginStates: state.pluginService.plugins,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenC
|
||||||
import { AppState } from './types';
|
import { AppState } from './types';
|
||||||
import { Store } from 'redux';
|
import { Store } from 'redux';
|
||||||
import editorCommandDeclarations from '../components/NoteEditor/commandDeclarations';
|
import editorCommandDeclarations from '../components/NoteEditor/commandDeclarations';
|
||||||
|
import globalCommands from '../commands';
|
||||||
import libCommands from '@joplin/lib/commands';
|
import libCommands from '@joplin/lib/commands';
|
||||||
|
|
||||||
interface CommandSpecification {
|
interface CommandSpecification {
|
||||||
|
@ -23,6 +24,7 @@ const initializeCommandService = (store: Store<AppState, any>) => {
|
||||||
for (const declaration of editorCommandDeclarations) {
|
for (const declaration of editorCommandDeclarations) {
|
||||||
CommandService.instance().registerDeclaration(declaration);
|
CommandService.instance().registerDeclaration(declaration);
|
||||||
}
|
}
|
||||||
|
registerCommands(globalCommands);
|
||||||
registerCommands(libCommands);
|
registerCommands(libCommands);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ const injectedJs = {
|
||||||
codeMirrorBundle: require('../lib/rnInjectedJs/codeMirrorBundle.bundle'),
|
codeMirrorBundle: require('../lib/rnInjectedJs/codeMirrorBundle.bundle'),
|
||||||
svgEditorBundle: require('../lib/rnInjectedJs/svgEditorBundle.bundle'),
|
svgEditorBundle: require('../lib/rnInjectedJs/svgEditorBundle.bundle'),
|
||||||
pluginBackgroundPage: require('../lib/rnInjectedJs/pluginBackgroundPage.bundle'),
|
pluginBackgroundPage: require('../lib/rnInjectedJs/pluginBackgroundPage.bundle'),
|
||||||
|
noteBodyViewerBundle: require('../lib/rnInjectedJs/noteBodyViewerBundle.bundle'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function shimInit() {
|
function shimInit() {
|
||||||
|
|
|
@ -78,6 +78,7 @@ export default class PluginLoader {
|
||||||
scriptElement.appendChild(document.createTextNode(`
|
scriptElement.appendChild(document.createTextNode(`
|
||||||
(async () => {
|
(async () => {
|
||||||
const exports = {};
|
const exports = {};
|
||||||
|
const module = {};
|
||||||
const require = window.__pluginLoaderRequireFunctions[${JSON.stringify(this.pluginLoaderId)}];
|
const require = window.__pluginLoaderRequireFunctions[${JSON.stringify(this.pluginLoaderId)}];
|
||||||
const joplin = {
|
const joplin = {
|
||||||
require,
|
require,
|
||||||
|
@ -85,7 +86,7 @@ export default class PluginLoader {
|
||||||
|
|
||||||
${js};
|
${js};
|
||||||
|
|
||||||
window.__pluginLoaderScriptLoadCallbacks[${JSON.stringify(scriptId)}](exports);
|
window.__pluginLoaderScriptLoadCallbacks[${JSON.stringify(scriptId)}](module.exports || exports);
|
||||||
})();
|
})();
|
||||||
`));
|
`));
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ export interface ContentScriptData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorControl {
|
export interface EditorControl {
|
||||||
supportsCommand(name: EditorCommandType|string): boolean;
|
supportsCommand(name: EditorCommandType|string): boolean|Promise<boolean>;
|
||||||
execCommand(name: EditorCommandType|string, ...args: any[]): void|Promise<any>;
|
execCommand(name: EditorCommandType|string, ...args: any[]): void|Promise<any>;
|
||||||
|
|
||||||
undo(): void;
|
undo(): void;
|
||||||
|
|
|
@ -1331,6 +1331,15 @@ class Setting extends BaseModel {
|
||||||
|
|
||||||
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] },
|
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] },
|
||||||
|
|
||||||
|
'ui.lastSelectedPluginPanel': {
|
||||||
|
value: '',
|
||||||
|
type: SettingItemType.String,
|
||||||
|
public: false,
|
||||||
|
description: () => 'The last selected plugin panel ID in pop-up mode (mobile).',
|
||||||
|
storage: SettingStorage.Database,
|
||||||
|
appTypes: [AppType.Mobile],
|
||||||
|
},
|
||||||
|
|
||||||
// TODO: Is there a better way to do this? The goal here is to simply have
|
// TODO: Is there a better way to do this? The goal here is to simply have
|
||||||
// a way to display a link to the customizable stylesheets, not for it to
|
// a way to display a link to the customizable stylesheets, not for it to
|
||||||
// serve as a customizable Setting. But because the Setting page is auto-
|
// serve as a customizable Setting. But because the Setting page is auto-
|
||||||
|
|
|
@ -10,7 +10,7 @@ async function processDirectory(dir, indexFilePath = null, typeScriptType = null
|
||||||
if (!importNameTemplate) importNameTemplate = '* as FILE_NAME';
|
if (!importNameTemplate) importNameTemplate = '* as FILE_NAME';
|
||||||
if (!exportNameTemplate) exportNameTemplate = 'FILE_NAME';
|
if (!exportNameTemplate) exportNameTemplate = 'FILE_NAME';
|
||||||
|
|
||||||
const tsFiles = glob.sync('{**/*.ts,**/*.tsx}', {
|
const tsFiles = glob.sync('{*.ts,*.tsx}', {
|
||||||
cwd: dir,
|
cwd: dir,
|
||||||
}).filter(f => `${dir}/${f}` !== indexFilePath)
|
}).filter(f => `${dir}/${f}` !== indexFilePath)
|
||||||
//
|
//
|
||||||
|
@ -67,6 +67,7 @@ module.exports = {
|
||||||
await processDirectory(`${rootDir}/packages/app-desktop/gui/NoteList/commands`);
|
await processDirectory(`${rootDir}/packages/app-desktop/gui/NoteList/commands`);
|
||||||
await processDirectory(`${rootDir}/packages/app-desktop/gui/NoteListControls/commands`);
|
await processDirectory(`${rootDir}/packages/app-desktop/gui/NoteListControls/commands`);
|
||||||
await processDirectory(`${rootDir}/packages/app-desktop/gui/Sidebar/commands`);
|
await processDirectory(`${rootDir}/packages/app-desktop/gui/Sidebar/commands`);
|
||||||
|
await processDirectory(`${rootDir}/packages/app-mobile/commands`);
|
||||||
await processDirectory(`${rootDir}/packages/lib/commands`);
|
await processDirectory(`${rootDir}/packages/lib/commands`);
|
||||||
|
|
||||||
await processDirectory(
|
await processDirectory(
|
||||||
|
|
Loading…
Reference in New Issue