From 98fce34fe91d6bf09890250105562b8727cf0c7f Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Thu, 9 Jan 2025 07:25:06 -0800 Subject: [PATCH] Web: Add support for auto-reloading dev plugins on change (#11545) --- .eslintignore | 1 + .gitignore | 1 + .../plugins/PluginRunnerWebView.tsx | 20 +++- .../plugins/utils/useOnDevPluginsUpdated.ts | 60 ++++++++++++ .../screens/ConfigScreen/ConfigScreen.tsx | 31 +++---- .../ConfigScreen/FileSystemPathSelector.tsx | 91 ++++++++++++++----- .../screens/ConfigScreen/SettingComponent.tsx | 12 ++- .../screens/ConfigScreen/SettingsToggle.tsx | 6 +- .../ConfigScreen/configScreenStyles.ts | 22 ++++- .../plugins/PluginBox/PluginChips.tsx | 8 ++ .../ConfigScreen/plugins/PluginInfoModal.tsx | 2 +- .../plugins/PluginUploadButton.tsx | 2 +- .../components/screens/ConfigScreen/types.ts | 2 +- .../lib/models/settings/builtInMetadata.ts | 18 +++- packages/lib/services/CommandService.ts | 4 + .../services/commands/ToolbarButtonUtils.ts | 12 ++- .../services/plugins/api/JoplinCommands.ts | 1 + packages/lib/services/plugins/loadPlugins.ts | 5 +- packages/lib/services/plugins/reducer.ts | 1 + .../api/references/mobile_plugin_debugging.md | 14 +++ 20 files changed, 249 insertions(+), 64 deletions(-) create mode 100644 packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.ts diff --git a/.eslintignore b/.eslintignore index 4aeb55e8370..79764cf70da 100644 --- a/.eslintignore +++ b/.eslintignore @@ -704,6 +704,7 @@ packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js packages/app-mobile/components/plugins/types.js packages/app-mobile/components/plugins/utils/createOnLogHandler.js +packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.js packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js diff --git a/.gitignore b/.gitignore index 71ed97babfd..18586771d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -679,6 +679,7 @@ packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js packages/app-mobile/components/plugins/types.js packages/app-mobile/components/plugins/utils/createOnLogHandler.js +packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.js packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js diff --git a/packages/app-mobile/components/plugins/PluginRunnerWebView.tsx b/packages/app-mobile/components/plugins/PluginRunnerWebView.tsx index 14a72c5deb3..c8edf4343fe 100644 --- a/packages/app-mobile/components/plugins/PluginRunnerWebView.tsx +++ b/packages/app-mobile/components/plugins/PluginRunnerWebView.tsx @@ -15,6 +15,7 @@ import { AppState } from '../../utils/types'; import usePrevious from '@joplin/lib/hooks/usePrevious'; import PlatformImplementation from '../../services/plugins/PlatformImplementation'; import AccessibleView from '../accessibility/AccessibleView'; +import useOnDevPluginsUpdated from './utils/useOnDevPluginsUpdated'; const logger = Logger.create('PluginRunnerWebView'); @@ -29,20 +30,33 @@ const usePlugins = ( pluginRunner: PluginRunner, webviewLoaded: boolean, pluginSettings: PluginSettings, + pluginSupportEnabled: boolean, + devPluginPath: string, ) => { const store = useStore(); const lastPluginRunner = usePrevious(pluginRunner); + const [reloadCounter, setReloadCounter] = useState(0); // Only set reloadAll to true here -- this ensures that all plugins are reloaded, // even if loadPlugins is cancelled and re-run. const reloadAllRef = useRef(false); reloadAllRef.current ||= pluginRunner !== lastPluginRunner; + useOnDevPluginsUpdated(async (pluginId: string) => { + logger.info(`Dev plugin ${pluginId} updated. Reloading...`); + await PluginService.instance().unloadPlugin(pluginId); + setReloadCounter(counter => counter + 1); + }, devPluginPath, pluginSupportEnabled); + useAsyncEffect(async (event) => { if (!webviewLoaded) { return; } + if (reloadCounter > 0) { + logger.debug('Reloading with counter set to', reloadCounter); + } + await loadPlugins({ pluginRunner, pluginSettings, @@ -56,7 +70,7 @@ const usePlugins = ( if (!event.cancelled) { reloadAllRef.current = false; } - }, [pluginRunner, store, webviewLoaded, pluginSettings]); + }, [pluginRunner, store, webviewLoaded, pluginSettings, reloadCounter]); }; const useUnloadPluginsOnGlobalDisable = ( @@ -79,6 +93,7 @@ interface Props { serializedPluginSettings: SerializedPluginSettings; pluginSupportEnabled: boolean; pluginStates: PluginStates; + devPluginPath: string; pluginHtmlContents: PluginHtmlContents; themeId: number; } @@ -98,7 +113,7 @@ const PluginRunnerWebViewComponent: React.FC = props => { }, [webviewReloadCounter]); const pluginSettings = usePluginSettings(props.serializedPluginSettings); - usePlugins(pluginRunner, webviewLoaded, pluginSettings); + usePlugins(pluginRunner, webviewLoaded, pluginSettings, props.pluginSupportEnabled, props.devPluginPath); useUnloadPluginsOnGlobalDisable(props.pluginStates, props.pluginSupportEnabled); const onLoadStart = useCallback(() => { @@ -183,6 +198,7 @@ export default connect((state: AppState) => { const result: Props = { serializedPluginSettings: state.settings['plugins.states'], pluginSupportEnabled: state.settings['plugins.pluginSupportEnabled'], + devPluginPath: state.settings['plugins.devPluginPaths'], pluginStates: state.pluginService.plugins, pluginHtmlContents: state.pluginService.pluginHtmlContents, themeId: state.settings.theme, diff --git a/packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.ts b/packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.ts new file mode 100644 index 00000000000..f1ad164bdd0 --- /dev/null +++ b/packages/app-mobile/components/plugins/utils/useOnDevPluginsUpdated.ts @@ -0,0 +1,60 @@ +import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; +import shim from '@joplin/lib/shim'; +import time from '@joplin/lib/time'; +import { basename, join } from 'path'; +import { useRef } from 'react'; + +type OnDevPluginChange = (id: string)=> void; + +const useOnDevPluginsUpdated = (onDevPluginChange: OnDevPluginChange, devPluginPath: string, pluginSupportEnabled: boolean) => { + const onDevPluginChangeRef = useRef(onDevPluginChange); + onDevPluginChangeRef.current = onDevPluginChange; + const isFirstUpdateRef = useRef(true); + + useAsyncEffect(async (event) => { + if (!devPluginPath || !pluginSupportEnabled) return; + + const itemToLastModTime = new Map(); + + // publishPath should point to the publish/ subfolder of a plugin's development + // directory. + const checkPluginChange = async (pluginPublishPath: string) => { + const dirStats = await shim.fsDriver().readDirStats(pluginPublishPath); + let hasChange = false; + let changedPluginId = ''; + for (const item of dirStats) { + if (item.path.endsWith('.jpl')) { + const lastModTime = itemToLastModTime.get(item.path); + const modTime = item.mtime.getTime(); + if (lastModTime === undefined || lastModTime < modTime) { + itemToLastModTime.set(item.path, modTime); + hasChange = true; + changedPluginId = basename(item.path, '.jpl'); + break; + } + } + } + + if (hasChange) { + if (isFirstUpdateRef.current) { + // Avoid sending an event the first time the hook is called. The first iteration + // collects initial timestamp information. In that case, hasChange + // will always be true, even with no plugin reload. + isFirstUpdateRef.current = false; + } else { + onDevPluginChangeRef.current(changedPluginId); + } + } + }; + + while (!event.cancelled) { + const publishFolder = join(devPluginPath, 'publish'); + await checkPluginChange(publishFolder); + + const pollingIntervalSeconds = 5; + await time.sleep(pollingIntervalSeconds); + } + }, [devPluginPath, pluginSupportEnabled]); +}; + +export default useOnDevPluginsUpdated; diff --git a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx index 4539d43cca3..1c849479033 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native'; +import { Platform, Linking, View, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native'; import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting'; import NavService from '@joplin/lib/services/NavService'; import SearchEngine from '@joplin/lib/services/search/SearchEngine'; @@ -12,7 +12,6 @@ import { connect } from 'react-redux'; import ScreenHeader from '../../ScreenHeader'; import { _ } from '@joplin/lib/locale'; import BaseScreenComponent from '../../base-screen'; -import { themeStyle } from '../../global-style'; import * as shared from '@joplin/lib/components/shared/config/config-shared'; import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; import biometricAuthenticate from '../../biometrics/biometricAuthenticate'; @@ -36,6 +35,8 @@ import EnablePluginSupportPage from './plugins/EnablePluginSupportPage'; import getVersionInfoText from '../../../utils/getVersionInfoText'; import JoplinCloudConfig, { emailToNoteDescription, emailToNoteLabel } from './JoplinCloudConfig'; import shim from '@joplin/lib/shim'; +import SettingsToggle from './SettingsToggle'; +import { UpdateSettingValueCallback } from './types'; interface ConfigScreenState { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -673,22 +674,16 @@ class ConfigScreenComponent extends BaseScreenComponent - - - {label} - - {/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */} - void updateSettingValue(key, value)} /> - - {descriptionComp} - - ); + private renderToggle(key: string, label: string, value: unknown, updateSettingValue: UpdateSettingValueCallback) { + return ; } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied diff --git a/packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.tsx b/packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.tsx index 675216b09ab..1bd64af243a 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.tsx @@ -3,18 +3,23 @@ import * as React from 'react'; import shim from '@joplin/lib/shim'; import { FunctionComponent, useCallback, useEffect, useState } from 'react'; import { ConfigScreenStyles } from './configScreenStyles'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet } from 'react-native'; import Setting, { SettingItem } from '@joplin/lib/models/Setting'; import { openDocumentTree } from '@joplin/react-native-saf-x'; import { UpdateSettingValueCallback } from './types'; import { reg } from '@joplin/lib/registry'; import type FsDriverWeb from '../../../utils/fs-driver/fs-driver-rn.web'; -import { TouchableRipple } from 'react-native-paper'; +import { IconButton, TouchableRipple } from 'react-native-paper'; +import { _ } from '@joplin/lib/locale'; + +type Mode = 'read'|'readwrite'; interface Props { + themeId: number; styles: ConfigScreenStyles; settingMetadata: SettingItem; - mode: 'read'|'readwrite'; + mode: Mode; + description: React.ReactNode|null; updateSettingValue: UpdateSettingValueCallback; } @@ -23,30 +28,28 @@ type ExtendedSelf = (typeof window.self) & { }; declare const self: ExtendedSelf; -const FileSystemPathSelector: FunctionComponent = props => { +const useFileSystemPath = (settingId: string, updateSettingValue: UpdateSettingValueCallback, accessMode: Mode) => { const [fileSystemPath, setFileSystemPath] = useState(''); - const settingId = props.settingMetadata.key; - useEffect(() => { setFileSystemPath(Setting.value(settingId)); }, [settingId]); - const selectDirectoryButtonPress = useCallback(async () => { + const showDirectoryPicker = useCallback(async () => { if (shim.mobilePlatform() === 'web') { // Directory picker IDs can't include certain characters. const pickerId = `setting-${settingId}`.replace(/[^a-zA-Z]/g, '_'); - const handle = await self.showDirectoryPicker({ id: pickerId, mode: props.mode }); + const handle = await self.showDirectoryPicker({ id: pickerId, mode: accessMode }); const fsDriver = shim.fsDriver() as FsDriverWeb; - const uri = await fsDriver.mountExternalDirectory(handle, pickerId, props.mode); - await props.updateSettingValue(settingId, uri); + const uri = await fsDriver.mountExternalDirectory(handle, pickerId, accessMode); + await updateSettingValue(settingId, uri); setFileSystemPath(uri); } else { try { const doc = await openDocumentTree(true); if (doc?.uri) { setFileSystemPath(doc.uri); - await props.updateSettingValue(settingId, doc.uri); + await updateSettingValue(settingId, doc.uri); } else { throw new Error('User cancelled operation'); } @@ -54,32 +57,78 @@ const FileSystemPathSelector: FunctionComponent = props => { reg.logger().info('Didn\'t pick sync dir: ', e); } } - }, [props.updateSettingValue, settingId, props.mode]); + }, [updateSettingValue, settingId, accessMode]); + + const clearPath = useCallback(() => { + setFileSystemPath(''); + void updateSettingValue(settingId, ''); + }, [updateSettingValue, settingId]); // Supported on Android and some versions of Chrome const supported = shim.fsDriver().isUsingAndroidSAF() || (shim.mobilePlatform() === 'web' && 'showDirectoryPicker' in self); - if (!supported) { - return null; - } + + return { clearPath, showDirectoryPicker, fileSystemPath, supported }; +}; + +const pathSelectorStyles = StyleSheet.create({ + innerContainer: { + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + paddingRight: 0, + }, + mainButton: { + flexGrow: 1, + flexShrink: 1, + paddingHorizontal: 16, + paddingVertical: 22, + margin: 0, + }, + buttonContent: { + flexDirection: 'row', + }, +}); + +const FileSystemPathSelector: FunctionComponent = props => { + const settingId = props.settingMetadata.key; + const { clearPath, showDirectoryPicker, fileSystemPath, supported } = useFileSystemPath(settingId, props.updateSettingValue, props.mode); const styleSheet = props.styles.styleSheet; - return ( + const clearButton = ( + + ); + + const containerStyles = props.styles.getContainerStyle(!!props.description); + + const control = - + {props.settingMetadata.label()} - + {fileSystemPath} - ); + {fileSystemPath ? clearButton : null} + ; + + if (!supported) return null; + + return + {control} + {props.description} + ; }; export default FileSystemPathSelector; diff --git a/packages/app-mobile/components/screens/ConfigScreen/SettingComponent.tsx b/packages/app-mobile/components/screens/ConfigScreen/SettingComponent.tsx index 74cd66e2b06..748ffdaace1 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/SettingComponent.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/SettingComponent.tsx @@ -38,7 +38,7 @@ const SettingComponent: React.FunctionComponent = props => { const styleSheet = props.styles.styleSheet; const descriptionComp = !settingDescription ? null : {settingDescription}; - const containerStyle = props.styles.getContainerStyle(!!settingDescription); + const containerStyles = props.styles.getContainerStyle(!!settingDescription); const labelId = useId(); @@ -49,8 +49,8 @@ const SettingComponent: React.FunctionComponent = props => { const label = md.label(); return ( - - + + {label} @@ -125,17 +125,19 @@ const SettingComponent: React.FunctionComponent = props => { if (['sync.2.path', 'plugins.devPluginPaths'].includes(md.key) && (shim.fsDriver().isUsingAndroidSAF() || shim.mobilePlatform() === 'web')) { return ( ); } return ( - - + + {md.label()} diff --git a/packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.tsx b/packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.tsx index a9c4f38cf2b..ebace0634b5 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.tsx @@ -24,9 +24,11 @@ const SettingsToggle: FunctionComponent = props => { const theme = themeStyle(props.themeId); const styleSheet = props.styles.styleSheet; + const containerStyles = props.styles.getContainerStyle(!!props.description); + return ( - - + + {props.label} diff --git a/packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.ts b/packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.ts index 2ddff9b9b9f..b243c7c091d 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.ts +++ b/packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.ts @@ -6,8 +6,11 @@ type SidebarButtonStyle = ViewStyle & { height: number }; export interface ConfigScreenStyleSheet { body: ViewStyle; + settingOuterContainer: ViewStyle; + settingOuterContainerNoBorder: ViewStyle; settingContainer: ViewStyle; settingContainerNoBottomBorder: ViewStyle; + headerWrapperStyle: ViewStyle; headerTextStyle: TextStyle; @@ -39,12 +42,17 @@ export interface ConfigScreenStyleSheet { settingControl: TextStyle; } +interface ContainerStyles { + outerContainer: ViewStyle; + innerContainer: ViewStyle; +} + export interface ConfigScreenStyles { styleSheet: ConfigScreenStyleSheet; selectedSectionButtonColor: string; keyboardAppearance: 'default'|'light'|'dark'; - getContainerStyle(hasDescription: boolean): ViewStyle; + getContainerStyle(hasDescription: boolean): ContainerStyles; } const configScreenStyles = (themeId: number): ConfigScreenStyles => { @@ -107,6 +115,14 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => { justifyContent: 'flex-start', flexDirection: 'column', }, + settingOuterContainer: { + flexDirection: 'column', + borderBottomWidth: 1, + borderBottomColor: theme.dividerColor, + }, + settingOuterContainerNoBorder: { + flexDirection: 'column', + }, settingContainer: settingContainerStyle, settingContainerNoBottomBorder: { ...settingContainerStyle, @@ -229,7 +245,9 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => { selectedSectionButtonColor: theme.selectedColor, keyboardAppearance: theme.keyboardAppearance, getContainerStyle: (hasDescription) => { - return !hasDescription ? styleSheet.settingContainer : styleSheet.settingContainerNoBottomBorder; + const outerContainer = hasDescription ? styleSheet.settingOuterContainer : styleSheet.settingOuterContainerNoBorder; + const innerContainer = hasDescription ? styleSheet.settingContainerNoBottomBorder : styleSheet.settingContainer; + return { outerContainer, innerContainer }; }, }; }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx index 9002609d350..cd4628030ca 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginChips.tsx @@ -92,12 +92,20 @@ const PluginChips: React.FC = props => { return {_('Installed')}; }; + const renderDevChip = () => { + if (!item.devMode) { + return null; + } + return {_('Dev')}; + }; + return {renderIncompatibleChip()} {renderInstalledChip()} {renderErrorsChip()} {renderBuiltInChip()} {renderUpdatableChip()} + {renderDevChip()} {renderDisabledChip()} ; }; diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx index ae6869e7353..dce8550d201 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginInfoModal.tsx @@ -203,7 +203,7 @@ const PluginInfoModalContent: React.FC = props => { item={item} type={ButtonType.Delete} onPress={props.pluginCallbacks.onDelete} - disabled={item.builtIn || (item?.deleted ?? true)} + disabled={item.builtIn || item.devMode || (item?.deleted ?? true)} title={item?.deleted ? _('Deleted') : _('Delete')} /> ); diff --git a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx index 1b46bf0fec7..0d59ac68b99 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/plugins/PluginUploadButton.tsx @@ -91,7 +91,7 @@ const PluginUploadButton: React.FC = props => { }, [props.pluginSettings, props.updatePluginStates]); return ( - + Promise; +export type UpdateSettingValueCallback = (key: string, value: any)=> void|Promise; export interface PluginStatusRecord { [pluginId: string]: boolean; diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index 8e5d8e05f73..e046547cda7 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -929,9 +929,23 @@ const builtInMetadata = (Setting: typeof SettingType) => { section: 'plugins', public: true, advanced: true, - appTypes: [AppType.Desktop], + appTypes: [AppType.Desktop, AppType.Mobile], + // For now, development plugins are only enabled on desktop & web. + show: (settings) => { + if (shim.isElectron()) return true; + if (shim.mobilePlatform() !== 'web') return false; + + const pluginSupportEnabled = settings['plugins.pluginSupportEnabled']; + return !!pluginSupportEnabled; + }, label: () => 'Development plugins', - description: () => 'You may add multiple plugin paths, each separated by a comma. You will need to restart the application for the changes to take effect.', + description: () => { + if (shim.mobilePlatform()) { + return 'The path to a plugin\'s development directory. When the plugin is rebuilt, Joplin reloads the plugin automatically.'; + } else { + return 'You may add multiple plugin paths, each separated by a comma. You will need to restart the application for the changes to take effect.'; + } + }, storage: SettingStorage.File, }, diff --git a/packages/lib/services/CommandService.ts b/packages/lib/services/CommandService.ts index 5a1fba54e27..7fa9b026b74 100644 --- a/packages/lib/services/CommandService.ts +++ b/packages/lib/services/CommandService.ts @@ -209,6 +209,10 @@ export default class CommandService extends BaseService { }; } + public unregisterDeclaration(name: string) { + delete this.commands_[name]; + } + public registerRuntime(commandName: string, runtime: CommandRuntime, allowMultiple = false): RegisteredRuntime { if (typeof commandName !== 'string') throw new Error(`Command name must be a string. Got: ${JSON.stringify(commandName)}`); diff --git a/packages/lib/services/commands/ToolbarButtonUtils.ts b/packages/lib/services/commands/ToolbarButtonUtils.ts index 554bb8b8832..a86a5766c84 100644 --- a/packages/lib/services/commands/ToolbarButtonUtils.ts +++ b/packages/lib/services/commands/ToolbarButtonUtils.ts @@ -48,22 +48,24 @@ export default class ToolbarButtonUtils { private commandToToolbarButton(commandName: string, whenClauseContext: WhenClauseContext): ToolbarButtonInfo { const newEnabled = this.service.isEnabled(commandName, whenClauseContext); const newTitle = this.service.title(commandName); + const newIcon = this.service.iconName(commandName); + const newLabel = this.service.label(commandName); if ( this.toolbarButtonCache_[commandName] && this.toolbarButtonCache_[commandName].info.enabled === newEnabled && - this.toolbarButtonCache_[commandName].info.title === newTitle + this.toolbarButtonCache_[commandName].info.title === newTitle && + this.toolbarButtonCache_[commandName].info.iconName === newIcon && + this.toolbarButtonCache_[commandName].info.tooltip === newLabel ) { return this.toolbarButtonCache_[commandName].info; } - const command = this.service.commandByName(commandName, { runtimeMustBeRegistered: true }); - const output: ToolbarButtonInfo = { type: 'button', name: commandName, - tooltip: this.service.label(commandName), - iconName: command.declaration.iconName, + tooltip: newLabel, + iconName: newIcon, enabled: newEnabled, onClick: async () => { await this.service.execute(commandName); diff --git a/packages/lib/services/plugins/api/JoplinCommands.ts b/packages/lib/services/plugins/api/JoplinCommands.ts index 1df70fe1352..7f4d31624e9 100644 --- a/packages/lib/services/plugins/api/JoplinCommands.ts +++ b/packages/lib/services/plugins/api/JoplinCommands.ts @@ -122,6 +122,7 @@ export default class JoplinCommands { CommandService.instance().registerRuntime(declaration.name, runtime); this.plugin_.addOnUnloadListener(() => { CommandService.instance().unregisterRuntime(declaration.name); + CommandService.instance().unregisterDeclaration(declaration.name); }); } diff --git a/packages/lib/services/plugins/loadPlugins.ts b/packages/lib/services/plugins/loadPlugins.ts index ac83b7cf001..f09cd84f8bf 100644 --- a/packages/lib/services/plugins/loadPlugins.ts +++ b/packages/lib/services/plugins/loadPlugins.ts @@ -51,10 +51,7 @@ const loadPlugins = async ({ } } - if (Setting.value('env') === 'dev') { - logger.info('Running dev plugins (if any)...'); - await pluginService.loadAndRunDevPlugins(pluginSettings); - } + await pluginService.loadAndRunDevPlugins(pluginSettings); if (cancelEvent.cancelled) { logger.info('Cancelled.'); diff --git a/packages/lib/services/plugins/reducer.ts b/packages/lib/services/plugins/reducer.ts index de9a3be7366..926e3fc0c58 100644 --- a/packages/lib/services/plugins/reducer.ts +++ b/packages/lib/services/plugins/reducer.ts @@ -202,6 +202,7 @@ const reducer = (draftRoot: Draft, action: any) => { case 'PLUGIN_UNLOAD': delete draft.plugins[action.pluginId]; + delete draft.pluginHtmlContents[action.pluginId]; break; } diff --git a/readme/api/references/mobile_plugin_debugging.md b/readme/api/references/mobile_plugin_debugging.md index 64ec33c5d32..de65f773aad 100644 --- a/readme/api/references/mobile_plugin_debugging.md +++ b/readme/api/references/mobile_plugin_debugging.md @@ -32,6 +32,20 @@ After loading, plugins are run in an `