1
0
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:
Laurent Cozic
2025-02-17 13:47:56 +00:00
committed by GitHub
parent d2aad1d6c7
commit c6154cfb4e
20 changed files with 337 additions and 164 deletions

View File

@ -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
View File

@ -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

View File

@ -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;

View File

@ -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]);
};

View File

@ -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,

View File

@ -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"

View File

@ -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);

View File

@ -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;
}

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -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');

View File

@ -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',
});
}
},
};
};

View File

@ -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);

View File

@ -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,

View 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);
}
}
}

View File

@ -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);

View File

@ -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> {

View File

@ -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 };
};

View File

@ -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.