一些检查失败了
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
191 行
5.9 KiB
TypeScript
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;
|