文件
mynode/packages/app-mobile/components/plugins/dialogs/PluginDialogWebView.tsx
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

191 行
5.9 KiB
TypeScript

import * as React from 'react';
import { useCallback, useMemo, useState } from 'react';
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
import { Dimensions, StyleSheet, View, useWindowDimensions } from 'react-native';
import usePlugin from '@joplin/lib/hooks/plugins/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';
import useKeyboardState from '../../../utils/hooks/useKeyboardState';
import useSafeAreaPadding from '../../../utils/hooks/useSafeAreaPadding';
interface Props {
themeId: number;
pluginHtmlContents: PluginHtmlContents;
pluginStates: PluginStates;
viewInfo: ViewInfo;
}
const useStyles = (
themeId: number,
dialogContentSize: DialogContentSize|null,
fitToContent: boolean,
) => {
const windowSize = useWindowDimensions();
const keyboardState = useKeyboardState();
const safeAreaPadding = useSafeAreaPadding();
return useMemo(() => {
const theme: Theme = themeStyle(themeId);
const useDialogSize = fitToContent && dialogContentSize;
const dialogHasLoaded = !!dialogContentSize;
const availableWidth = Math.max(
0,
windowSize.width - safeAreaPadding.paddingLeft - safeAreaPadding.paddingRight,
);
const maxWidth = availableWidth * 0.97;
// Opening the keyboard may resize the window fully, partially, or not at all.
// Use the smaller height so the dialog stays above any remaining keyboard
// overlap without deducting the keyboard twice.
const screenHeight = Dimensions.get('screen').height;
// Convert the screen-relative keyboard position to window coordinates. Cap the
// offset because the screen/window height difference may also include an IME resize.
const screenToWindowOffset = Math.min(
safeAreaPadding.paddingBottom,
Math.max(0, screenHeight - windowSize.height),
);
const keyboardTop = screenHeight - screenToWindowOffset - keyboardState.dockedKeyboardHeight;
const bottomPadding = safeAreaPadding.paddingBottom;
const availableHeight = Math.max(
0,
Math.min(windowSize.height, keyboardTop) - safeAreaPadding.paddingTop - bottomPadding,
);
const maxHeight = availableHeight * 0.97;
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, keyboardState.dockedKeyboardHeight, safeAreaPadding]);
};
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: formData as Record<string, unknown> });
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;