You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
Mobile: Add support for plugin editor views (#11831)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
This commit is contained in:
@ -456,7 +456,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
||||
@ -465,7 +464,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditorPlugin.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
||||
@ -1022,7 +1020,9 @@ packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/permanentlyDeleteNote.js
|
||||
packages/lib/commands/renderMarkup.test.js
|
||||
packages/lib/commands/renderMarkup.js
|
||||
packages/lib/commands/showEditorPlugin.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
@ -1258,6 +1258,7 @@ packages/lib/services/ocr/utils/filterOcrText.js
|
||||
packages/lib/services/ocr/utils/types.js
|
||||
packages/lib/services/plugins/BasePlatformImplementation.js
|
||||
packages/lib/services/plugins/BasePluginRunner.js
|
||||
packages/lib/services/plugins/EditorPluginHandler.js
|
||||
packages/lib/services/plugins/MenuController.js
|
||||
packages/lib/services/plugins/MenuItemController.js
|
||||
packages/lib/services/plugins/Plugin.js
|
||||
@ -1304,6 +1305,7 @@ packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorView.js
|
||||
packages/lib/services/plugins/utils/isCompatible/getDefaultPlatforms.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.test.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.js
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -431,7 +431,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showEditorPlugin.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js
|
||||
@ -440,7 +439,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditorPlugin.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js
|
||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js
|
||||
@ -997,7 +995,9 @@ packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/permanentlyDeleteNote.js
|
||||
packages/lib/commands/renderMarkup.test.js
|
||||
packages/lib/commands/renderMarkup.js
|
||||
packages/lib/commands/showEditorPlugin.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/commands/toggleEditorPlugin.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.test.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
|
||||
@ -1233,6 +1233,7 @@ packages/lib/services/ocr/utils/filterOcrText.js
|
||||
packages/lib/services/ocr/utils/types.js
|
||||
packages/lib/services/plugins/BasePlatformImplementation.js
|
||||
packages/lib/services/plugins/BasePluginRunner.js
|
||||
packages/lib/services/plugins/EditorPluginHandler.js
|
||||
packages/lib/services/plugins/MenuController.js
|
||||
packages/lib/services/plugins/MenuItemController.js
|
||||
packages/lib/services/plugins/Plugin.js
|
||||
@ -1279,6 +1280,7 @@ packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
packages/lib/services/plugins/utils/getShownPluginEditorView.js
|
||||
packages/lib/services/plugins/utils/isCompatible/getDefaultPlatforms.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.test.js
|
||||
packages/lib/services/plugins/utils/isCompatible/index.js
|
||||
|
@ -52,10 +52,8 @@ import Logger from '@joplin/utils/Logger';
|
||||
import usePluginEditorView from './utils/usePluginEditorView';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { WindowIdContext } from '../NewWindowOrIFrame';
|
||||
import { EditorActivationCheckFilterObject } from '@joplin/lib/services/plugins/api/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
|
||||
import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue';
|
||||
import EditorPluginHandler from '@joplin/lib/services/plugins/EditorPluginHandler';
|
||||
import useResourceUnwatcher from './utils/useResourceUnwatcher';
|
||||
import StatusBar from './StatusBar';
|
||||
|
||||
@ -72,15 +70,6 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||
const onDragOver: React.DragEventHandler = event => event.preventDefault();
|
||||
let editorIdCounter = 0;
|
||||
|
||||
const makeNoteUpdateAction = (shownEditorViewIds: string[]) => {
|
||||
return async () => {
|
||||
for (const viewId of shownEditorViewIds) {
|
||||
const controller = PluginService.instance().viewControllerByViewId(viewId) as WebviewController;
|
||||
if (controller) controller.emitUpdate();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function NoteEditorContent(props: NoteEditorProps) {
|
||||
const [showRevisions, setShowRevisions] = useState(false);
|
||||
const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false);
|
||||
@ -90,7 +79,10 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
const titleInputRef = useRef<HTMLInputElement>();
|
||||
const isMountedRef = useRef(true);
|
||||
const noteSearchBarRef = useRef(null);
|
||||
const viewUpdateAsyncQueue_ = useRef<AsyncActionQueue>(new AsyncActionQueue(100, IntervalType.Fixed));
|
||||
|
||||
const editorPluginHandler = useMemo(() => {
|
||||
return new EditorPluginHandler(PluginService.instance());
|
||||
}, []);
|
||||
|
||||
const shownEditorViewIds = props['plugins.shownEditorViewIds'];
|
||||
|
||||
@ -114,25 +106,15 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
|
||||
const effectiveNoteId = useEffectiveNoteId(props);
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
useAsyncEffect(async (_event) => {
|
||||
if (!props.startupPluginsLoaded) return;
|
||||
|
||||
let filterObject: EditorActivationCheckFilterObject = {
|
||||
activatedEditors: [],
|
||||
};
|
||||
filterObject = await eventManager.filterEmit('editorActivationCheck', filterObject);
|
||||
if (event.cancelled) return;
|
||||
|
||||
for (const editor of filterObject.activatedEditors) {
|
||||
const controller = PluginService.instance().pluginById(editor.pluginId).viewController(editor.viewId) as WebviewController;
|
||||
controller.setActive(editor.isActive);
|
||||
}
|
||||
}, [effectiveNoteId, props.startupPluginsLoaded]);
|
||||
await editorPluginHandler.emitActivationCheck();
|
||||
}, [effectiveNoteId, editorPluginHandler, props.startupPluginsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.startupPluginsLoaded) return;
|
||||
viewUpdateAsyncQueue_.current.push(makeNoteUpdateAction(shownEditorViewIds));
|
||||
}, [effectiveNoteId, shownEditorViewIds, props.startupPluginsLoaded]);
|
||||
editorPluginHandler.emitUpdate(shownEditorViewIds);
|
||||
}, [effectiveNoteId, editorPluginHandler, shownEditorViewIds, props.startupPluginsLoaded]);
|
||||
|
||||
const { editorPlugin, editorView } = usePluginEditorView(props.plugins, shownEditorViewIds);
|
||||
const builtInEditorVisible = !editorPlugin;
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||
import getShownPluginEditorView from '@joplin/lib/services/plugins/utils/getShownPluginEditorView';
|
||||
|
||||
// If a plugin editor should be shown for the current note, this function will return the plugin and
|
||||
// associated view.
|
||||
export default (plugins: PluginStates, shownEditorViewIds: string[]) => {
|
||||
return useMemo(() => {
|
||||
const { editorPlugin, editorView } = getActivePluginEditorView(plugins);
|
||||
if (editorView) {
|
||||
if (!shownEditorViewIds.includes(editorView.id)) return { editorPlugin: null, editorView: null };
|
||||
}
|
||||
return { editorPlugin, editorView };
|
||||
return getShownPluginEditorView(plugins, shownEditorViewIds);
|
||||
}, [plugins, shownEditorViewIds]);
|
||||
};
|
||||
|
@ -28,7 +28,6 @@ import * as restoreNote from './restoreNote';
|
||||
import * as revealResourceFile from './revealResourceFile';
|
||||
import * as search from './search';
|
||||
import * as setTags from './setTags';
|
||||
import * as showEditorPlugin from './showEditorPlugin';
|
||||
import * as showModalMessage from './showModalMessage';
|
||||
import * as showNoteContentProperties from './showNoteContentProperties';
|
||||
import * as showNoteProperties from './showNoteProperties';
|
||||
@ -36,7 +35,6 @@ import * as showPrompt from './showPrompt';
|
||||
import * as showShareFolderDialog from './showShareFolderDialog';
|
||||
import * as showShareNoteDialog from './showShareNoteDialog';
|
||||
import * as showSpellCheckerMenu from './showSpellCheckerMenu';
|
||||
import * as toggleEditorPlugin from './toggleEditorPlugin';
|
||||
import * as toggleEditors from './toggleEditors';
|
||||
import * as toggleLayoutMoveMode from './toggleLayoutMoveMode';
|
||||
import * as toggleMenuBar from './toggleMenuBar';
|
||||
@ -78,7 +76,6 @@ const index: any[] = [
|
||||
revealResourceFile,
|
||||
search,
|
||||
setTags,
|
||||
showEditorPlugin,
|
||||
showModalMessage,
|
||||
showNoteContentProperties,
|
||||
showNoteProperties,
|
||||
@ -86,7 +83,6 @@ const index: any[] = [
|
||||
showShareFolderDialog,
|
||||
showShareNoteDialog,
|
||||
showSpellCheckerMenu,
|
||||
toggleEditorPlugin,
|
||||
toggleEditors,
|
||||
toggleLayoutMoveMode,
|
||||
toggleMenuBar,
|
||||
|
@ -6,7 +6,7 @@
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
TEMP_PATH=~/src/plugin-tests
|
||||
NEED_COMPILING=1
|
||||
PLUGIN_PATH=~/src/joplin/packages/app-cli/tests/support/plugins/toast
|
||||
PLUGIN_PATH=~/src/plugin-yesyoucan
|
||||
|
||||
if [[ $NEED_COMPILING == 1 ]]; then
|
||||
mkdir -p "$TEMP_PATH"
|
||||
|
@ -25,6 +25,7 @@ import WebBetaButton from './WebBetaButton';
|
||||
|
||||
import Menu, { MenuOptionType } from './Menu';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
export { MenuOptionType };
|
||||
|
||||
// Rather than applying a padding to the whole bar, it is applied to each
|
||||
@ -67,6 +68,7 @@ interface ScreenHeaderProps {
|
||||
showSideMenuButton?: boolean;
|
||||
showSearchButton?: boolean;
|
||||
showContextMenuButton?: boolean;
|
||||
showPluginEditorButton?: boolean;
|
||||
showBackButton?: boolean;
|
||||
|
||||
saveButtonDisabled?: boolean;
|
||||
@ -419,6 +421,25 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const renderTogglePluginEditorButton = (styles: any, onPress: OnPressCallback, disabled: boolean) => {
|
||||
if (!this.props.showPluginEditorButton) return null;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
|
||||
themeId={themeId}
|
||||
description={_('Toggle plugin editor')}
|
||||
contentWrapperStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
|
||||
|
||||
iconName='ionicon eye'
|
||||
iconStyle={styles.topIcon}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function deleteButton(styles: any, onPress: OnPressCallback, disabled: boolean) {
|
||||
return (
|
||||
@ -604,12 +625,14 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
const restoreButtonComp = selectedFolderInTrash && this.props.noteSelectionEnabled ? restoreButton(this.styles(), () => this.restoreButton_press(), headerItemDisabled) : null;
|
||||
const duplicateButtonComp = !selectedFolderInTrash && this.props.noteSelectionEnabled ? duplicateButton(this.styles(), () => this.duplicateButton_press(), headerItemDisabled) : null;
|
||||
const sortButtonComp = !this.props.noteSelectionEnabled && this.props.sortButton_press ? sortButton(this.styles(), () => this.props.sortButton_press()) : null;
|
||||
const togglePluginEditorButton = renderTogglePluginEditorButton(this.styles(), () => CommandService.instance().execute('toggleEditorPlugin'), false);
|
||||
|
||||
// To allow the notebook dropdown (and perhaps other components) to have sufficient
|
||||
// space while in use, we allow certain buttons to be hidden.
|
||||
const hideableRightComponents = <>
|
||||
{pluginPanelsComp}
|
||||
{betaIconComp}
|
||||
{togglePluginEditorButton}
|
||||
</>;
|
||||
|
||||
const titleComp = createTitleComponent(hideableRightComponents);
|
||||
|
@ -28,7 +28,7 @@ const PluginDialogManager: React.FC<Props> = props => {
|
||||
|
||||
const dialogs: ReactElement[] = [];
|
||||
for (const viewInfo of viewInfos) {
|
||||
if (viewInfo.view.containerType === ContainerType.Panel || !viewInfo.view.opened) {
|
||||
if (viewInfo.view.containerType !== ContainerType.Dialog || !viewInfo.view.opened) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
import { getDisplayParentTitle } from '@joplin/lib/services/trash';
|
||||
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import { PluginHtmlContents, PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import debounce from '../../../utils/debounce';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import CommandService, { RegisteredRuntime } from '@joplin/lib/services/CommandService';
|
||||
@ -63,6 +63,11 @@ import { DialogContext, DialogControl } from '../../DialogManager';
|
||||
import { CommandRuntimeProps, EditorMode, PickerResponse } from './types';
|
||||
import commands from './commands';
|
||||
import { AttachFileAction, AttachFileOptions } from './commands/attachFile';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import PluginUserWebView from '../../plugins/dialogs/PluginUserWebView';
|
||||
import getShownPluginEditorView from '@joplin/lib/services/plugins/utils/getShownPluginEditorView';
|
||||
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||
import EditorPluginHandler from '@joplin/lib/services/plugins/EditorPluginHandler';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const emptyArray: any[] = [];
|
||||
@ -97,6 +102,9 @@ interface Props extends BaseProps {
|
||||
highlightedWords: string[];
|
||||
noteHash: string;
|
||||
toolbarEnabled: boolean;
|
||||
'plugins.shownEditorViewIds': string[];
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
editorNoteReloadTimeRequest: number;
|
||||
}
|
||||
|
||||
interface ComponentProps extends Props {
|
||||
@ -122,6 +130,7 @@ interface State {
|
||||
imageEditorResourceFilepath: string;
|
||||
noteResources: Record<string, ResourceInfo>;
|
||||
newAndNoTitleChangeNoteId: boolean|null;
|
||||
noteLastLoadTime: number;
|
||||
|
||||
undoRedoButtonState: {
|
||||
canUndo: boolean;
|
||||
@ -162,6 +171,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public dialogbox: any;
|
||||
private commandRegistration_: RegisteredRuntime|null = null;
|
||||
private editorPluginHandler_ = new EditorPluginHandler(PluginService.instance());
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
public static navigationOptions(): any {
|
||||
@ -189,6 +199,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
noteResources: {},
|
||||
imageEditorResourceFilepath: null,
|
||||
newAndNoTitleChangeNoteId: null,
|
||||
noteLastLoadTime: Date.now(),
|
||||
|
||||
undoRedoButtonState: {
|
||||
canUndo: false,
|
||||
@ -551,6 +562,12 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
await this.editorPluginHandler_.emitActivationCheck();
|
||||
|
||||
setTimeout(() => {
|
||||
this.editorPluginHandler_.emitUpdate(this.props['plugins.shownEditorViewIds']);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@ -610,6 +627,17 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
noteHash: noteHash,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props['plugins.shownEditorViewIds'] !== prevProps['plugins.shownEditorViewIds']) {
|
||||
const { editorPlugin } = getShownPluginEditorView(this.props.plugins, this.props['plugins.shownEditorViewIds']);
|
||||
if (!editorPlugin && this.props.editorNoteReloadTimeRequest > this.state.noteLastLoadTime) {
|
||||
void shared.reloadNote(this);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevProps.noteId && this.props.noteId && prevProps.noteId !== this.props.noteId) {
|
||||
void this.editorPluginHandler_.emitActivationCheck();
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
@ -1420,6 +1448,8 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
// multiple times.
|
||||
this.registerCommands();
|
||||
|
||||
const { editorPlugin, editorView } = getShownPluginEditorView(this.props.plugins, this.props['plugins.shownEditorViewIds']);
|
||||
|
||||
if (this.state.isLoading) {
|
||||
return (
|
||||
<View style={this.styles().screen}>
|
||||
@ -1448,10 +1478,25 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
/>;
|
||||
}
|
||||
|
||||
const renderPluginEditor = () => {
|
||||
return <PluginUserWebView
|
||||
viewInfo={{ plugin: editorPlugin, view: editorView }}
|
||||
themeId={this.props.themeId}
|
||||
onLoadEnd={() => {}}
|
||||
pluginHtmlContents={this.props.pluginHtmlContents}
|
||||
setDialogControl={() => {}}
|
||||
style={{}}
|
||||
/>;
|
||||
};
|
||||
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
const keywords = this.props.searchQuery && !!this.props.ftsEnabled ? this.props.highlightedWords : emptyArray;
|
||||
|
||||
let bodyComponent = null;
|
||||
|
||||
if (editorView) {
|
||||
bodyComponent = renderPluginEditor();
|
||||
} else {
|
||||
if (this.state.mode === 'view') {
|
||||
// Note: as of 2018-12-29 it's important not to display the viewer if the note body is empty,
|
||||
// to avoid the HACK_webviewLoadingState related bug.
|
||||
@ -1542,6 +1587,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const renderActionButton = () => {
|
||||
if (this.state.voiceTypingDialogShown) return null;
|
||||
@ -1596,6 +1642,8 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
return <VoiceTypingDialog locale={currentLocale()} onText={this.voiceTypingDialog_onText} onDismiss={this.voiceTypingDialog_onDismiss}/>;
|
||||
};
|
||||
|
||||
const { editorPlugin: activeEditorPlugin } = getActivePluginEditorView(this.props.plugins);
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.themeId).root}>
|
||||
<ScreenHeader
|
||||
@ -1608,6 +1656,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
showSearchButton={false}
|
||||
showUndoButton={(this.state.undoRedoButtonState.canUndo || this.state.undoRedoButtonState.canRedo) && this.state.mode === 'edit'}
|
||||
showRedoButton={this.state.undoRedoButtonState.canRedo && this.state.mode === 'edit'}
|
||||
showPluginEditorButton={!!activeEditorPlugin}
|
||||
undoButtonDisabled={!this.state.undoRedoButtonState.canUndo && this.state.undoRedoButtonState.canRedo}
|
||||
onUndoButtonPress={this.screenHeader_undoButtonPress}
|
||||
onRedoButtonPress={this.screenHeader_redoButtonPress}
|
||||
@ -1655,6 +1704,9 @@ const NoteScreen = connect((state: AppState) => {
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
'plugins.shownEditorViewIds': state.settings['plugins.shownEditorViewIds'] || [],
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
editorNoteReloadTimeRequest: state.editorNoteReloadTimeRequest,
|
||||
|
||||
// What we call "beta editor" in this component is actually the (now
|
||||
// default) CodeMirror editor. That should be refactored to make it less
|
||||
|
@ -288,7 +288,14 @@ class NotesScreenComponent extends BaseScreenComponent<ComponentProps, State> {
|
||||
|
||||
inert={accessibilityHidden}
|
||||
>
|
||||
<ScreenHeader title={iconString + title} showBackButton={false} sortButton_press={this.sortButton_press} folderPickerOptions={this.folderPickerOptions()} showSearchButton={true} showSideMenuButton={true} />
|
||||
<ScreenHeader
|
||||
title={iconString + title}
|
||||
showBackButton={false}
|
||||
sortButton_press={this.sortButton_press}
|
||||
folderPickerOptions={this.folderPickerOptions()}
|
||||
showSearchButton={true}
|
||||
showSideMenuButton={true}
|
||||
/>
|
||||
<NoteList />
|
||||
{actionButtonComp}
|
||||
</AccessibleView>
|
||||
|
@ -5,7 +5,9 @@ import * as historyForward from './historyForward';
|
||||
import * as openMasterPasswordDialog from './openMasterPasswordDialog';
|
||||
import * as permanentlyDeleteNote from './permanentlyDeleteNote';
|
||||
import * as renderMarkup from './renderMarkup';
|
||||
import * as showEditorPlugin from './showEditorPlugin';
|
||||
import * as synchronize from './synchronize';
|
||||
import * as toggleEditorPlugin from './toggleEditorPlugin';
|
||||
|
||||
const index: any[] = [
|
||||
deleteNote,
|
||||
@ -14,7 +16,9 @@ const index: any[] = [
|
||||
openMasterPasswordDialog,
|
||||
permanentlyDeleteNote,
|
||||
renderMarkup,
|
||||
showEditorPlugin,
|
||||
synchronize,
|
||||
toggleEditorPlugin,
|
||||
];
|
||||
|
||||
export default index;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||
import { CommandContext, CommandDeclaration, CommandRuntime } from '../services/CommandService';
|
||||
import Setting from '../models/Setting';
|
||||
import getActivePluginEditorView from '../services/plugins/utils/getActivePluginEditorView';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('showEditorPlugin');
|
@ -1,7 +1,7 @@
|
||||
import { CommandContext, CommandDeclaration, CommandRuntime } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import getActivePluginEditorView from '@joplin/lib/services/plugins/utils/getActivePluginEditorView';
|
||||
import { CommandContext, CommandDeclaration, CommandRuntime } from '../services/CommandService';
|
||||
import { _ } from '../locale';
|
||||
import Setting from '../models/Setting';
|
||||
import getActivePluginEditorView from '../services/plugins/utils/getActivePluginEditorView';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('toggleEditorPlugin');
|
||||
@ -24,14 +24,26 @@ export const runtime = (): CommandRuntime => {
|
||||
}
|
||||
|
||||
const idx = shownEditorViewIds.indexOf(editorView.id);
|
||||
let hasBeenHidden = false;
|
||||
|
||||
if (idx < 0) {
|
||||
shownEditorViewIds.push(editorView.id);
|
||||
} else {
|
||||
shownEditorViewIds.splice(idx, 1);
|
||||
hasBeenHidden = true;
|
||||
}
|
||||
|
||||
logger.info('New shown editor views: ', shownEditorViewIds);
|
||||
|
||||
Setting.setValue('plugins.shownEditorViewIds', shownEditorViewIds);
|
||||
|
||||
if (hasBeenHidden) {
|
||||
// When the plugin editor goes from visible to hidden, we need to reload the note
|
||||
// because it may have been changed via the data API.
|
||||
context.dispatch({
|
||||
type: 'EDITOR_NOTE_NEEDS_RELOAD',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
@ -67,6 +67,8 @@ interface Shared {
|
||||
installResourceHandling?: (refreshResourceHandler: any)=> void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
uninstallResourceHandling?: (refreshResourceHandler: any)=> void;
|
||||
|
||||
reloadNote?: (comp: BaseNoteScreenComponent)=> Promise<NoteEntity>;
|
||||
}
|
||||
|
||||
const shared: Shared = {};
|
||||
@ -268,7 +270,7 @@ shared.isModified = function(comp: BaseNoteScreenComponent) {
|
||||
return !!Object.getOwnPropertyNames(diff).length;
|
||||
};
|
||||
|
||||
shared.initState = async function(comp: BaseNoteScreenComponent) {
|
||||
shared.reloadNote = async (comp: BaseNoteScreenComponent) => {
|
||||
const isProvisionalNote = comp.props.provisionalNoteIds.includes(comp.props.noteId);
|
||||
|
||||
const note = await Note.load(comp.props.noteId);
|
||||
@ -292,6 +294,7 @@ shared.initState = async function(comp: BaseNoteScreenComponent) {
|
||||
fromShare: !!comp.props.sharedData,
|
||||
noteResources: await shared.attachedResources(note ? note.body : ''),
|
||||
readOnly: itemIsReadOnlySync(ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, note as ItemSlice, Setting.value('sync.userId'), BaseItem.syncShareCache),
|
||||
noteLastLoadTime: Date.now(),
|
||||
});
|
||||
} else {
|
||||
// Handle the case where a non-existent note is loaded. This can happen briefly after deleting a note.
|
||||
@ -304,9 +307,16 @@ shared.initState = async function(comp: BaseNoteScreenComponent) {
|
||||
fromShare,
|
||||
noteResources: [],
|
||||
readOnly: true,
|
||||
noteLastLoadTime: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return note;
|
||||
};
|
||||
|
||||
shared.initState = async function(comp: BaseNoteScreenComponent) {
|
||||
const note = await shared.reloadNote(comp);
|
||||
|
||||
if (comp.props.sharedData) {
|
||||
if (comp.props.sharedData.title) {
|
||||
this.noteComponent_change(comp, 'title', comp.props.sharedData.title);
|
||||
|
@ -170,6 +170,7 @@ export interface State extends WindowState {
|
||||
mustUpgradeAppMessage: string;
|
||||
mustAuthenticate: boolean;
|
||||
toast: Toast | null;
|
||||
editorNoteReloadTimeRequest: number;
|
||||
|
||||
allowSelectionInOtherFolders: boolean;
|
||||
|
||||
@ -241,6 +242,7 @@ export const defaultState: State = {
|
||||
mustUpgradeAppMessage: '',
|
||||
mustAuthenticate: false,
|
||||
allowSelectionInOtherFolders: false,
|
||||
editorNoteReloadTimeRequest: 0,
|
||||
|
||||
pluginService: pluginServiceDefaultState,
|
||||
shareService: shareServiceDefaultState,
|
||||
@ -1512,6 +1514,12 @@ const reducer = produce((draft: Draft<State> = defaultState, action: any) => {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'EDITOR_NOTE_NEEDS_RELOAD':
|
||||
{
|
||||
draft.editorNoteReloadTimeRequest = Date.now();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'TOAST_SHOW':
|
||||
draft.toast = {
|
||||
duration: 6000,
|
||||
|
52
packages/lib/services/plugins/EditorPluginHandler.ts
Normal file
52
packages/lib/services/plugins/EditorPluginHandler.ts
Normal file
@ -0,0 +1,52 @@
|
||||
// The goal of this class is to simplify the integration of the `joplin.views.editor` plugin logic
|
||||
// in the desktop and mobile app. See here for more information:
|
||||
//
|
||||
// packages/lib/services/plugins/api/JoplinViewsEditor.ts
|
||||
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import AsyncActionQueue, { IntervalType } from '../../AsyncActionQueue';
|
||||
import eventManager from '../../eventManager';
|
||||
import { EditorActivationCheckFilterObject } from './api/types';
|
||||
import type PluginService from './PluginService';
|
||||
import WebviewController from './WebviewController';
|
||||
|
||||
const logger = Logger.create('EditorPluginHandler');
|
||||
|
||||
const makeNoteUpdateAction = (pluginService: PluginService, shownEditorViewIds: string[]) => {
|
||||
return async () => {
|
||||
for (const viewId of shownEditorViewIds) {
|
||||
const controller = pluginService.viewControllerByViewId(viewId) as WebviewController;
|
||||
if (controller) controller.emitUpdate();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default class {
|
||||
|
||||
private pluginService_: PluginService;
|
||||
private viewUpdateAsyncQueue_ = new AsyncActionQueue(100, IntervalType.Fixed);
|
||||
|
||||
public constructor(pluginService: PluginService) {
|
||||
this.pluginService_ = pluginService;
|
||||
}
|
||||
|
||||
public emitUpdate(shownEditorViewIds: string[]) {
|
||||
logger.info('emitUpdate:', shownEditorViewIds);
|
||||
this.viewUpdateAsyncQueue_.push(makeNoteUpdateAction(this.pluginService_, shownEditorViewIds));
|
||||
}
|
||||
|
||||
public async emitActivationCheck() {
|
||||
let filterObject: EditorActivationCheckFilterObject = {
|
||||
activatedEditors: [],
|
||||
};
|
||||
filterObject = await eventManager.filterEmit('editorActivationCheck', filterObject);
|
||||
|
||||
logger.info('emitActivationCheck: responses:', filterObject);
|
||||
|
||||
for (const editor of filterObject.activatedEditors) {
|
||||
const controller = this.pluginService_.pluginById(editor.pluginId).viewController(editor.viewId) as WebviewController;
|
||||
controller.setActive(editor.isActive);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -279,7 +279,7 @@ export default class WebviewController extends ViewController {
|
||||
return this.storeView.opened;
|
||||
}
|
||||
|
||||
public async isVisible(): Promise<boolean> {
|
||||
public isVisible(): boolean {
|
||||
if (!this.storeView.opened) return false;
|
||||
const shownEditorViewIds: string[] = this.store.getState().settings['plugins.shownEditorViewIds'];
|
||||
return shownEditorViewIds.includes(this.handle);
|
||||
|
@ -93,10 +93,10 @@ export default class JoplinViewsEditors {
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when the editor can potentially be activated - this for example when the current note
|
||||
* is changed, or when the application is opened. At that point should can check the current
|
||||
* note and decide whether your editor should be activated or not. If it should return `true`,
|
||||
* otherwise return `false`.
|
||||
* Emitted when the editor can potentially be activated - this is for example when the current
|
||||
* note is changed, or when the application is opened. At that point you should check the
|
||||
* current note and decide whether your editor should be activated or not. If it should, return
|
||||
* `true`, otherwise return `false`.
|
||||
*/
|
||||
public async onActivationCheck(handle: ViewHandle, callback: ActivationCheckCallback): Promise<void> {
|
||||
const handler: FilterHandler<EditorActivationCheckFilterObject> = async (object) => {
|
||||
@ -118,7 +118,7 @@ export default class JoplinViewsEditors {
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when the editor content should be updated. This for example when the currently
|
||||
* Emitted when your editor content should be updated. This is for example when the currently
|
||||
* selected note changes, or when the user makes the editor visible.
|
||||
*/
|
||||
public async onUpdate(handle: ViewHandle, callback: UpdateCallback): Promise<void> {
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { PluginStates } from '../reducer';
|
||||
import getActivePluginEditorView from './getActivePluginEditorView';
|
||||
|
||||
export default (plugins: PluginStates, shownEditorViewIds: string[]) => {
|
||||
const { editorPlugin, editorView } = getActivePluginEditorView(plugins);
|
||||
if (editorView) {
|
||||
if (!shownEditorViewIds.includes(editorView.id)) return { editorPlugin: null, editorView: null };
|
||||
}
|
||||
return { editorPlugin, editorView };
|
||||
};
|
@ -18,29 +18,46 @@ Variables follow the naming convention `--joplin-{property}` and are used in you
|
||||
|
||||
## Icons
|
||||
|
||||
In addition to variables, you have access to a set of standard font assets that ship with Joplin. These include:
|
||||
On desktop, your plugin view will have access to icons used by the app. It is however not recommended to use them because they may change in future versions. And it will also make your plugin incompatible with the mobile app (which does not expose any icon library).
|
||||
|
||||
* [Roboto](https://fonts.google.com/specimen/Roboto?preview.text_type=custom) - (the standard UI font, `font-family` referenced above)
|
||||
* [Font Awesome](https://fontawesome.com/icons?d=gallery&p=2&m=free) - icon library
|
||||
* [icoMoon](https://icomoon.io/#preview-free) - icon library (subset, see [style.css](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/style/icons/style.css))
|
||||
Instead a recommended approach is to add Font Awesome in your plugin project, and to import only the icons you'll need. To do so using React, follow these instructions:
|
||||
|
||||
To display an icon, use CSS and HTML like the following.
|
||||
**Install Font Awesome:**
|
||||
|
||||
```css
|
||||
/* style icons to match the theme */
|
||||
.toolbarIcon {
|
||||
font-size: var(--joplin-toolbar-icon-size);
|
||||
}
|
||||
.primary {
|
||||
color: var(--joplin-color);
|
||||
}
|
||||
.secondary {
|
||||
color: var(--joplin-color2);
|
||||
}
|
||||
```shell
|
||||
npm install --save @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/free-regular-svg-icons @fortawesome/react-fontawesome
|
||||
```
|
||||
|
||||
```html
|
||||
<i class="toolbarIcon primary fas fa-music"></i> Font Awesome music icon
|
||||
<br />
|
||||
<i class="toolbarIcon secondary icon-notebooks"></i> icoMoon notebook icon
|
||||
**Import and load the icons:**
|
||||
|
||||
From one of your top TypeScript files:
|
||||
|
||||
```typescript
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
// Import the specific icons you want to use
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCheckCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
// Add the icons to the library
|
||||
library.add(faTimes, faCheckCircle);
|
||||
```
|
||||
|
||||
**Use Font Awesome React Components:**
|
||||
|
||||
```JSX
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<div>
|
||||
<FontAwesomeIcon icon="times" />
|
||||
<FontAwesomeIcon icon={['far', 'check-circle']} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
If you are not using React, just ask ChatGPT on how to do the above using you preferred JS framework.
|
Reference in New Issue
Block a user