You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
Chore: Refactor mobile plugin logic into locations more consistent with other parts of the app (#10636)
This commit is contained in:
@ -0,0 +1,62 @@
|
||||
import * as React from 'react';
|
||||
import { ReactElement } from 'react';
|
||||
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||
import PluginDialogWebView from './PluginDialogWebView';
|
||||
import { Modal, Portal } from 'react-native-paper';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import useViewInfos from './hooks/useViewInfos';
|
||||
import PluginPanelViewer from './PluginPanelViewer';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
pluginStates: PluginStates;
|
||||
}
|
||||
|
||||
const dismissDialog = (viewInfo: ViewInfo) => {
|
||||
if (!viewInfo.view.opened) return;
|
||||
|
||||
const plugin = PluginService.instance().pluginById(viewInfo.plugin.id);
|
||||
const viewController = plugin.viewController(viewInfo.view.id) as WebviewController;
|
||||
viewController.closeWithResponse(null);
|
||||
};
|
||||
|
||||
const PluginDialogManager: React.FC<Props> = props => {
|
||||
const viewInfos = useViewInfos(props.pluginStates);
|
||||
|
||||
const dialogs: ReactElement[] = [];
|
||||
for (const viewInfo of viewInfos) {
|
||||
if (viewInfo.view.containerType === ContainerType.Panel || !viewInfo.view.opened) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dialogs.push(
|
||||
<Portal
|
||||
key={`${viewInfo.plugin.id}-${viewInfo.view.id}`}
|
||||
>
|
||||
<Modal
|
||||
visible={true}
|
||||
onDismiss={() => dismissDialog(viewInfo)}
|
||||
>
|
||||
<PluginDialogWebView
|
||||
viewInfo={viewInfo}
|
||||
themeId={props.themeId}
|
||||
pluginStates={props.pluginStates}
|
||||
pluginHtmlContents={props.pluginHtmlContents}
|
||||
/>
|
||||
</Modal>
|
||||
</Portal>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{dialogs}
|
||||
<PluginPanelViewer/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginDialogManager;
|
@ -0,0 +1,166 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||
import { StyleSheet, View, useWindowDimensions } from 'react-native';
|
||||
import usePlugin from '@joplin/lib/hooks/usePlugin';
|
||||
import { DialogContentSize, DialogWebViewApi } from '../types';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { ButtonSpec, DialogResult } from '@joplin/lib/services/plugins/api/types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import useDialogSize from './hooks/useDialogSize';
|
||||
import PluginUserWebView from './PluginUserWebView';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
pluginStates: PluginStates;
|
||||
viewInfo: ViewInfo;
|
||||
}
|
||||
|
||||
const useStyles = (
|
||||
themeId: number,
|
||||
dialogContentSize: DialogContentSize|null,
|
||||
fitToContent: boolean,
|
||||
) => {
|
||||
const windowSize = useWindowDimensions();
|
||||
|
||||
return useMemo(() => {
|
||||
const theme: Theme = themeStyle(themeId);
|
||||
|
||||
const useDialogSize = fitToContent && dialogContentSize;
|
||||
const dialogHasLoaded = !!dialogContentSize;
|
||||
|
||||
const maxWidth = windowSize.width * 0.97;
|
||||
const maxHeight = windowSize.height * 0.95;
|
||||
const dialogWidth = useDialogSize ? dialogContentSize.width : maxWidth;
|
||||
const dialogHeight = useDialogSize ? dialogContentSize.height : maxHeight;
|
||||
|
||||
return StyleSheet.create({
|
||||
webView: {
|
||||
backgroundColor: 'transparent',
|
||||
display: 'flex',
|
||||
},
|
||||
webViewContainer: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
width: dialogWidth,
|
||||
height: dialogHeight,
|
||||
},
|
||||
dialog: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
borderRadius: 12,
|
||||
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
opacity: dialogHasLoaded ? 1 : 0.1,
|
||||
|
||||
// Center
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
flexShrink: 0,
|
||||
padding: 12,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
});
|
||||
}, [themeId, dialogContentSize, fitToContent, windowSize.width, windowSize.height]);
|
||||
};
|
||||
|
||||
const defaultButtonSpecs: ButtonSpec[] = [
|
||||
{ id: 'ok' }, { id: 'cancel' },
|
||||
];
|
||||
|
||||
const PluginDialogWebView: React.FC<Props> = props => {
|
||||
const viewInfo = props.viewInfo;
|
||||
const view = viewInfo.view;
|
||||
const pluginId = viewInfo.plugin.id;
|
||||
const viewId = view.id;
|
||||
const plugin = usePlugin(pluginId);
|
||||
const [webViewLoadCount, setWebViewLoadCount] = useState(0);
|
||||
const [dialogControl, setDialogControl] = useState<DialogWebViewApi|null>(null);
|
||||
|
||||
const dialogSize = useDialogSize({
|
||||
dialogControl,
|
||||
webViewLoadCount,
|
||||
watchForSizeChanges: view.fitToContent,
|
||||
});
|
||||
const styles = useStyles(props.themeId, dialogSize, view.fitToContent);
|
||||
|
||||
const onButtonPress = useCallback(async (button: ButtonSpec) => {
|
||||
const closeWithResponse = (response?: DialogResult|null) => {
|
||||
const viewController = plugin.viewController(viewId) as WebviewController;
|
||||
if (view.containerType === ContainerType.Dialog) {
|
||||
viewController.closeWithResponse(response);
|
||||
} else {
|
||||
viewController.close();
|
||||
}
|
||||
};
|
||||
|
||||
let formData = undefined;
|
||||
if (button.id !== 'cancel') {
|
||||
formData = await dialogControl.getFormData();
|
||||
}
|
||||
|
||||
closeWithResponse({ id: button.id, formData });
|
||||
button.onClick?.();
|
||||
}, [dialogControl, plugin, viewId, view.containerType]);
|
||||
|
||||
const buttonComponents: React.ReactElement[] = [];
|
||||
const buttonSpecs = view.buttons ?? defaultButtonSpecs;
|
||||
|
||||
for (const button of buttonSpecs) {
|
||||
let iconName = undefined;
|
||||
let buttonTitle = button.title ?? button.id;
|
||||
|
||||
if (button.id === 'cancel') {
|
||||
iconName = 'close-outline';
|
||||
buttonTitle = button.title ?? _('Cancel');
|
||||
} else if (button.id === 'ok') {
|
||||
iconName = 'check';
|
||||
buttonTitle = button.title ?? _('OK');
|
||||
}
|
||||
|
||||
buttonComponents.push(
|
||||
<Button
|
||||
key={button.id}
|
||||
icon={iconName}
|
||||
mode='text'
|
||||
onPress={() => onButtonPress(button)}
|
||||
>
|
||||
{buttonTitle}
|
||||
</Button>,
|
||||
);
|
||||
}
|
||||
|
||||
const onWebViewLoaded = useCallback(() => {
|
||||
setWebViewLoadCount(webViewLoadCount + 1);
|
||||
}, [setWebViewLoadCount, webViewLoadCount]);
|
||||
|
||||
return (
|
||||
<View style={styles.dialog}>
|
||||
<View style={styles.webViewContainer}>
|
||||
<PluginUserWebView
|
||||
style={styles.webView}
|
||||
themeId={props.themeId}
|
||||
viewInfo={props.viewInfo}
|
||||
pluginHtmlContents={props.pluginHtmlContents}
|
||||
onLoadEnd={onWebViewLoaded}
|
||||
setDialogControl={setDialogControl}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.buttonRow}>
|
||||
{buttonComponents}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginDialogWebView;
|
@ -0,0 +1,188 @@
|
||||
|
||||
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||
import * as React from 'react';
|
||||
import { Button, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
||||
import useViewInfos from './hooks/useViewInfos';
|
||||
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import PluginUserWebView from './PluginUserWebView';
|
||||
import { View, StyleSheet, AccessibilityInfo } from 'react-native';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { Dispatch } from 'redux';
|
||||
import DismissibleDialog, { DialogSize } from '../../../components/DismissibleDialog';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
pluginStates: PluginStates;
|
||||
visible: boolean;
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
webView: {
|
||||
backgroundColor: 'transparent',
|
||||
display: 'flex',
|
||||
},
|
||||
webViewContainer: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
});
|
||||
|
||||
type ButtonInfo = {
|
||||
value: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
const useSelectedTabId = (
|
||||
buttonInfos: ButtonInfo[],
|
||||
viewInfoById: Record<string, ViewInfo>,
|
||||
) => {
|
||||
const viewInfoByIdRef = useRef(viewInfoById);
|
||||
viewInfoByIdRef.current = viewInfoById;
|
||||
|
||||
const getDefaultSelectedTabId = useCallback((): string|undefined => {
|
||||
const lastSelectedId = Setting.value('ui.lastSelectedPluginPanel');
|
||||
const lastSelectedInfo = viewInfoByIdRef.current[lastSelectedId];
|
||||
if (lastSelectedId && lastSelectedInfo && lastSelectedInfo.view.opened) {
|
||||
return lastSelectedId;
|
||||
} else {
|
||||
return buttonInfos[0]?.value;
|
||||
}
|
||||
}, [buttonInfos]);
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState<string|undefined>(getDefaultSelectedTabId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTabId || !viewInfoById[selectedTabId]?.view?.opened) {
|
||||
setSelectedTabId(getDefaultSelectedTabId());
|
||||
}
|
||||
}, [selectedTabId, viewInfoById, getDefaultSelectedTabId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTabId) return;
|
||||
|
||||
const info = viewInfoByIdRef.current[selectedTabId];
|
||||
Setting.setValue('ui.lastSelectedPluginPanel', selectedTabId);
|
||||
AccessibilityInfo.announceForAccessibility(_('%s tab opened', getTabLabel(info)));
|
||||
}, [selectedTabId]);
|
||||
|
||||
return { setSelectedTabId, selectedTabId };
|
||||
};
|
||||
|
||||
const emptyCallback = () => {};
|
||||
|
||||
const getTabLabel = (info: ViewInfo) => {
|
||||
// Handles the case where a plugin just unloaded or hasn't loaded yet.
|
||||
if (!info) {
|
||||
return '...';
|
||||
}
|
||||
|
||||
return PluginService.instance().pluginById(info.plugin.id).manifest.name;
|
||||
};
|
||||
const PluginPanelViewer: React.FC<Props> = props => {
|
||||
const viewInfos = useViewInfos(props.pluginStates);
|
||||
const viewInfoById = useMemo(() => {
|
||||
const result: Record<string, ViewInfo> = {};
|
||||
for (const info of viewInfos) {
|
||||
result[info.view.id] = info;
|
||||
}
|
||||
return result;
|
||||
}, [viewInfos]);
|
||||
|
||||
const buttonInfos = useMemo(() => {
|
||||
return Object.entries(viewInfoById)
|
||||
.filter(([_id, info]) => info.view.containerType === ContainerType.Panel)
|
||||
.filter(([_id, info]) => info.view.opened)
|
||||
.map(([id, info]) => {
|
||||
return {
|
||||
value: id,
|
||||
label: getTabLabel(info),
|
||||
icon: 'puzzle',
|
||||
};
|
||||
});
|
||||
}, [viewInfoById]);
|
||||
|
||||
const { selectedTabId, setSelectedTabId } = useSelectedTabId(buttonInfos, viewInfoById);
|
||||
|
||||
const viewInfo = viewInfoById[selectedTabId];
|
||||
|
||||
const renderTabContent = () => {
|
||||
if (!viewInfo) {
|
||||
return <Text>{_('No tab selected')}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.webViewContainer}>
|
||||
<PluginUserWebView
|
||||
key={selectedTabId}
|
||||
themeId={props.themeId}
|
||||
style={styles.webView}
|
||||
viewInfo={viewInfo}
|
||||
pluginHtmlContents={props.pluginHtmlContents}
|
||||
onLoadEnd={emptyCallback}
|
||||
setDialogControl={emptyCallback}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTabSelector = () => {
|
||||
// SegmentedButtons doesn't display correctly when there's only one button.
|
||||
// As such, we include a special case:
|
||||
if (buttonInfos.length === 1) {
|
||||
const buttonInfo = buttonInfos[0];
|
||||
return (
|
||||
<Button icon={buttonInfo.icon} onPress={()=>setSelectedTabId(buttonInfo.value)}>
|
||||
{buttonInfo.label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SegmentedButtons
|
||||
value={selectedTabId}
|
||||
onValueChange={setSelectedTabId}
|
||||
buttons={buttonInfos}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
props.dispatch({
|
||||
type: 'SET_PLUGIN_PANELS_DIALOG_VISIBLE',
|
||||
visible: false,
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<DismissibleDialog
|
||||
themeId={props.themeId}
|
||||
visible={props.visible}
|
||||
size={DialogSize.Large}
|
||||
onDismiss={onClose}
|
||||
>
|
||||
{renderTabContent()}
|
||||
{renderTabSelector()}
|
||||
</DismissibleDialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect((state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
visible: state.showPanelsDialog,
|
||||
pluginStates: state.pluginService.plugins,
|
||||
};
|
||||
})(PluginPanelViewer);
|
@ -0,0 +1,132 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PluginHtmlContents, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||
import ExtendedWebView, { WebViewControl } from '../../../components/ExtendedWebView';
|
||||
import { ViewStyle } from 'react-native';
|
||||
import usePlugin from '@joplin/lib/hooks/usePlugin';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import useDialogMessenger from './hooks/useDialogMessenger';
|
||||
import useWebViewSetup from './hooks/useWebViewSetup';
|
||||
import { DialogWebViewApi } from '../types';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
viewInfo: ViewInfo;
|
||||
style: ViewStyle;
|
||||
onLoadEnd: ()=> void;
|
||||
setDialogControl: (dialogControl: DialogWebViewApi)=> void;
|
||||
}
|
||||
|
||||
const PluginUserWebView = (props: Props) => {
|
||||
const viewInfo = props.viewInfo;
|
||||
const view = viewInfo.view;
|
||||
const pluginId = viewInfo.plugin.id;
|
||||
const viewId = view.id;
|
||||
const plugin = usePlugin(pluginId);
|
||||
const [webViewLoadCount, setWebViewLoadCount] = useState(0);
|
||||
|
||||
const webviewRef = useRef<WebViewControl>(null);
|
||||
|
||||
const messageChannelId = `dialog-messenger-${pluginId}-${viewId}`;
|
||||
const messenger = useDialogMessenger({
|
||||
pluginId,
|
||||
viewId,
|
||||
webviewRef,
|
||||
messageChannelId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Because of how messenger.remoteApi handles message forwarding (property names
|
||||
// are not known), we need to send methods individually and can't use an object
|
||||
// spread or send messenger.remoteApi.
|
||||
props.setDialogControl({
|
||||
includeCssFiles: messenger.remoteApi.includeCssFiles,
|
||||
includeJsFiles: messenger.remoteApi.includeJsFiles,
|
||||
setThemeCss: messenger.remoteApi.setThemeCss,
|
||||
getFormData: messenger.remoteApi.getFormData,
|
||||
getContentSize: messenger.remoteApi.getContentSize,
|
||||
});
|
||||
}, [messenger, props.setDialogControl]);
|
||||
|
||||
useWebViewSetup({
|
||||
themeId: props.themeId,
|
||||
dialogControl: messenger.remoteApi,
|
||||
scriptPaths: view.scripts ?? [],
|
||||
pluginBaseDir: plugin.baseDir,
|
||||
webViewLoadCount,
|
||||
});
|
||||
|
||||
const htmlContent = props.pluginHtmlContents[pluginId]?.[viewId] ?? '';
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>Plugin Dialog</title>
|
||||
<style>
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--joplin-color);
|
||||
|
||||
/* -apple-system-body allows for correct font scaling on iOS devices */
|
||||
font: -apple-system-body;
|
||||
font-family: var(--joplin-font-family, sans-serif);
|
||||
}
|
||||
|
||||
/* We need "display: flex" in order to accurately get the content size */
|
||||
/* including margin and padding of children */
|
||||
#joplin-plugin-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="joplin-plugin-content">
|
||||
${htmlContent}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const injectedJs = useMemo(() => {
|
||||
return `
|
||||
if (!window.backgroundPageLoaded) {
|
||||
${shim.injectedJs('pluginBackgroundPage')}
|
||||
pluginBackgroundPage.initializeDialogWebView(
|
||||
${JSON.stringify(messageChannelId)}
|
||||
);
|
||||
|
||||
window.backgroundPageLoaded = true;
|
||||
}
|
||||
`;
|
||||
}, [messageChannelId]);
|
||||
|
||||
const onWebViewLoaded = useCallback(() => {
|
||||
setWebViewLoadCount(webViewLoadCount + 1);
|
||||
props.onLoadEnd();
|
||||
messenger.onWebViewLoaded();
|
||||
}, [messenger, setWebViewLoadCount, webViewLoadCount, props.onLoadEnd]);
|
||||
|
||||
return (
|
||||
<ExtendedWebView
|
||||
style={props.style}
|
||||
baseDirectory={plugin.baseDir}
|
||||
webviewInstanceId='joplin__PluginDialogWebView'
|
||||
html={html}
|
||||
hasPluginScripts={true}
|
||||
injectedJavaScript={injectedJs}
|
||||
onMessage={messenger.onWebViewMessage}
|
||||
onLoadEnd={onWebViewLoaded}
|
||||
ref={webviewRef}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginUserWebView;
|
@ -0,0 +1,53 @@
|
||||
import { useMemo, RefObject } from 'react';
|
||||
import { DialogMainProcessApi, DialogWebViewApi } from '../../types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { WebViewControl } from '../../../../components/ExtendedWebView';
|
||||
import createOnLogHander from '../../utils/createOnLogHandler';
|
||||
import RNToWebViewMessenger from '../../../../utils/ipc/RNToWebViewMessenger';
|
||||
import { SerializableData } from '@joplin/lib/utils/ipc/types';
|
||||
import PostMessageService, { ResponderComponentType } from '@joplin/lib/services/PostMessageService';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
|
||||
interface Props {
|
||||
pluginId: string;
|
||||
viewId: string;
|
||||
webviewRef: RefObject<WebViewControl>;
|
||||
messageChannelId: string;
|
||||
}
|
||||
|
||||
const useDialogMessenger = (props: Props) => {
|
||||
const { pluginId, webviewRef, viewId, messageChannelId } = props;
|
||||
|
||||
return useMemo(() => {
|
||||
const plugin = PluginService.instance().pluginById(pluginId);
|
||||
const logger = Logger.create(`PluginDialogWebView(${pluginId})`);
|
||||
|
||||
const dialogApi: DialogMainProcessApi = {
|
||||
postMessage: async (message: SerializableData) => {
|
||||
return await plugin.viewController(viewId).emitMessage({ message });
|
||||
},
|
||||
onMessage: async (callback) => {
|
||||
PostMessageService.instance().registerViewMessageHandler(
|
||||
ResponderComponentType.UserWebview,
|
||||
viewId,
|
||||
(message: SerializableData) => {
|
||||
// For compatibility with desktop, the message needs to be wrapped in
|
||||
// an object.
|
||||
return callback({ message });
|
||||
},
|
||||
);
|
||||
},
|
||||
onError: async (error: string) => {
|
||||
logger.error(`Unhandled error: ${error}`);
|
||||
plugin.hasErrors = true;
|
||||
},
|
||||
onLog: createOnLogHander(plugin, logger),
|
||||
};
|
||||
|
||||
return new RNToWebViewMessenger<DialogMainProcessApi, DialogWebViewApi>(
|
||||
messageChannelId, webviewRef, dialogApi,
|
||||
);
|
||||
}, [webviewRef, pluginId, viewId, messageChannelId]);
|
||||
};
|
||||
|
||||
export default useDialogMessenger;
|
@ -0,0 +1,52 @@
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { useRef, useState } from 'react';
|
||||
import { DialogContentSize, DialogWebViewApi } from '../../types';
|
||||
import { PixelRatio } from 'react-native';
|
||||
|
||||
interface Props {
|
||||
dialogControl: DialogWebViewApi;
|
||||
webViewLoadCount: number;
|
||||
watchForSizeChanges: boolean;
|
||||
}
|
||||
|
||||
const useDialogSize = (props: Props) => {
|
||||
const { dialogControl, webViewLoadCount } = props;
|
||||
|
||||
const [dialogSize, setDialogSize] = useState<DialogContentSize|null>(null);
|
||||
const lastSizeRef = useRef(dialogSize);
|
||||
lastSizeRef.current = dialogSize;
|
||||
useAsyncEffect(async event => {
|
||||
if (!dialogControl) {
|
||||
// May happen if the webview is still loading.
|
||||
return;
|
||||
}
|
||||
|
||||
while (!event.cancelled) {
|
||||
const contentSize = await dialogControl.getContentSize();
|
||||
if (event.cancelled) return;
|
||||
|
||||
const lastSize = lastSizeRef.current;
|
||||
if (contentSize.width !== lastSize?.width || contentSize.height !== lastSize?.height) {
|
||||
// We use 1000 here because getPixelSizeForLayoutSize is guaranteed to return
|
||||
// an integer.
|
||||
const pixelToDpRatio = 1000 / PixelRatio.getPixelSizeForLayoutSize(1000);
|
||||
setDialogSize({
|
||||
width: contentSize.width * pixelToDpRatio,
|
||||
height: contentSize.height * pixelToDpRatio,
|
||||
});
|
||||
}
|
||||
|
||||
if (!props.watchForSizeChanges) return;
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
}, [dialogControl, setDialogSize, webViewLoadCount]);
|
||||
|
||||
return dialogSize;
|
||||
};
|
||||
|
||||
export default useDialogSize;
|
@ -0,0 +1,10 @@
|
||||
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const useViewInfos = (pluginStates: PluginStates) => {
|
||||
return useMemo(() => {
|
||||
return pluginUtils.viewInfosByType(pluginStates, 'webview');
|
||||
}, [pluginStates]);
|
||||
};
|
||||
|
||||
export default useViewInfos;
|
@ -0,0 +1,43 @@
|
||||
import { useEffect } from 'react';
|
||||
import { DialogWebViewApi } from '../../types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { themeStyle } from '../../../../components/global-style';
|
||||
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
scriptPaths: string[];
|
||||
dialogControl: DialogWebViewApi;
|
||||
pluginBaseDir: string;
|
||||
|
||||
// Whenever the WebView reloads, we need to re-inject CSS and JavaScript.
|
||||
webViewLoadCount: number;
|
||||
}
|
||||
|
||||
const useWebViewSetup = (props: Props) => {
|
||||
const { scriptPaths, dialogControl, pluginBaseDir, themeId } = props;
|
||||
|
||||
useEffect(() => {
|
||||
const jsPaths = [];
|
||||
const cssPaths = [];
|
||||
for (const rawPath of scriptPaths) {
|
||||
const resolvedPath = shim.fsDriver().resolveRelativePathWithinDir(pluginBaseDir, rawPath);
|
||||
|
||||
if (resolvedPath.match(/\.css$/i)) {
|
||||
cssPaths.push(resolvedPath);
|
||||
} else {
|
||||
jsPaths.push(resolvedPath);
|
||||
}
|
||||
}
|
||||
void dialogControl.includeCssFiles(cssPaths);
|
||||
void dialogControl.includeJsFiles(jsPaths);
|
||||
}, [dialogControl, scriptPaths, props.webViewLoadCount, pluginBaseDir]);
|
||||
|
||||
useEffect(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
const themeVariableCss = themeToCss(theme);
|
||||
void dialogControl.setThemeCss(themeVariableCss);
|
||||
}, [dialogControl, themeId, props.webViewLoadCount]);
|
||||
};
|
||||
|
||||
export default useWebViewSetup;
|
Reference in New Issue
Block a user