2024-03-14 21:04:32 +02:00
|
|
|
|
|
|
|
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
|
|
|
import * as React from 'react';
|
2024-03-25 13:39:48 +02:00
|
|
|
import { Button, IconButton, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
2024-03-14 21:04:32 +02:00
|
|
|
import useViewInfos from './hooks/useViewInfos';
|
|
|
|
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
2024-03-25 13:39:48 +02:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
2024-03-14 21:04:32 +02:00
|
|
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
|
|
|
import { connect } from 'react-redux';
|
|
|
|
import { AppState } from '../../../utils/types';
|
|
|
|
import PluginUserWebView from './PluginUserWebView';
|
2024-03-25 13:39:48 +02:00
|
|
|
import { View, useWindowDimensions, StyleSheet, AccessibilityInfo } from 'react-native';
|
2024-03-14 21:04:32 +02:00
|
|
|
import { _ } from '@joplin/lib/locale';
|
|
|
|
import { Theme } from '@joplin/lib/themes/type';
|
|
|
|
import { themeStyle } from '@joplin/lib/theme';
|
|
|
|
import Setting from '@joplin/lib/models/Setting';
|
2024-03-25 13:39:48 +02:00
|
|
|
import { Dispatch } from 'redux';
|
|
|
|
import Modal from '../../../components/Modal';
|
2024-03-14 21:04:32 +02:00
|
|
|
|
|
|
|
interface Props {
|
|
|
|
themeId: number;
|
|
|
|
|
|
|
|
pluginHtmlContents: PluginHtmlContents;
|
|
|
|
pluginStates: PluginStates;
|
|
|
|
visible: boolean;
|
2024-03-25 13:39:48 +02:00
|
|
|
dispatch: Dispatch;
|
2024-03-14 21:04:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const useStyles = (themeId: number) => {
|
|
|
|
const windowSize = useWindowDimensions();
|
|
|
|
|
|
|
|
return useMemo(() => {
|
|
|
|
const theme: Theme = themeStyle(themeId);
|
|
|
|
|
|
|
|
return StyleSheet.create({
|
|
|
|
webView: {
|
|
|
|
backgroundColor: 'transparent',
|
|
|
|
display: 'flex',
|
|
|
|
},
|
|
|
|
webViewContainer: {
|
|
|
|
flexGrow: 1,
|
|
|
|
flexShrink: 1,
|
|
|
|
},
|
|
|
|
closeButtonContainer: {
|
|
|
|
flexDirection: 'row',
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
},
|
|
|
|
dialog: {
|
|
|
|
backgroundColor: theme.backgroundColor,
|
|
|
|
borderRadius: 12,
|
|
|
|
padding: 10,
|
|
|
|
|
|
|
|
height: windowSize.height * 0.9,
|
|
|
|
width: windowSize.width * 0.97,
|
|
|
|
|
|
|
|
// Center
|
|
|
|
marginLeft: 'auto',
|
|
|
|
marginRight: 'auto',
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}, [themeId, windowSize.width, windowSize.height]);
|
|
|
|
};
|
|
|
|
|
2024-03-25 13:39:48 +02:00
|
|
|
type ButtonInfo = {
|
|
|
|
value: string;
|
|
|
|
label: string;
|
|
|
|
icon: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
const useSelectedTabId = (buttonInfos: ButtonInfo[], viewInfoById: Record<string, ViewInfo>) => {
|
|
|
|
const getDefaultSelectedTabId = useCallback((): string|undefined => {
|
|
|
|
const lastSelectedId = Setting.value('ui.lastSelectedPluginPanel');
|
|
|
|
if (lastSelectedId && viewInfoById[lastSelectedId]) {
|
|
|
|
return lastSelectedId;
|
|
|
|
} else {
|
|
|
|
return buttonInfos[0]?.value;
|
|
|
|
}
|
|
|
|
}, [buttonInfos, viewInfoById]);
|
|
|
|
|
|
|
|
const [selectedTabId, setSelectedTabId] = useState<string|undefined>(getDefaultSelectedTabId);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!selectedTabId) {
|
|
|
|
setSelectedTabId(getDefaultSelectedTabId());
|
|
|
|
}
|
|
|
|
}, [selectedTabId, getDefaultSelectedTabId]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!selectedTabId) return () => {};
|
|
|
|
|
|
|
|
const info = viewInfoById[selectedTabId];
|
|
|
|
|
|
|
|
let controller: WebviewController|null = null;
|
|
|
|
if (info && info.view.opened) {
|
|
|
|
const plugin = PluginService.instance().pluginById(info.plugin.id);
|
|
|
|
controller = plugin.viewController(info.view.id) as WebviewController;
|
|
|
|
controller.setIsShownInModal(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
Setting.setValue('ui.lastSelectedPluginPanel', selectedTabId);
|
|
|
|
AccessibilityInfo.announceForAccessibility(_('%s tab opened', getTabLabel(info)));
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
controller?.setIsShownInModal(false);
|
|
|
|
};
|
|
|
|
}, [viewInfoById, selectedTabId]);
|
|
|
|
|
|
|
|
return { setSelectedTabId, selectedTabId };
|
|
|
|
};
|
|
|
|
|
2024-03-14 21:04:32 +02:00
|
|
|
const emptyCallback = () => {};
|
|
|
|
|
2024-03-25 13:39:48 +02:00
|
|
|
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;
|
|
|
|
};
|
2024-03-14 21:04:32 +02:00
|
|
|
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.plugin.id}--${info.view.id}`] = info;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}, [viewInfos]);
|
|
|
|
|
|
|
|
const buttonInfos = useMemo(() => {
|
|
|
|
return Object.entries(viewInfoById)
|
|
|
|
.filter(([_id, info]) => info.view.containerType === ContainerType.Panel)
|
|
|
|
.map(([id, info]) => {
|
|
|
|
return {
|
|
|
|
value: id,
|
2024-03-25 13:39:48 +02:00
|
|
|
label: getTabLabel(info),
|
2024-03-14 21:04:32 +02:00
|
|
|
icon: 'puzzle',
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}, [viewInfoById]);
|
|
|
|
|
|
|
|
const styles = useStyles(props.themeId);
|
2024-03-25 13:39:48 +02:00
|
|
|
const { selectedTabId, setSelectedTabId } = useSelectedTabId(buttonInfos, viewInfoById);
|
2024-03-14 21:04:32 +02:00
|
|
|
|
|
|
|
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];
|
2024-03-25 13:39:48 +02:00
|
|
|
return (
|
|
|
|
<Button icon={buttonInfo.icon} onPress={()=>setSelectedTabId(buttonInfo.value)}>
|
|
|
|
{buttonInfo.label}
|
|
|
|
</Button>
|
|
|
|
);
|
2024-03-14 21:04:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<SegmentedButtons
|
|
|
|
value={selectedTabId}
|
|
|
|
onValueChange={setSelectedTabId}
|
|
|
|
buttons={buttonInfos}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-03-25 13:39:48 +02:00
|
|
|
const onClose = useCallback(() => {
|
|
|
|
props.dispatch({
|
|
|
|
type: 'TOGGLE_PLUGIN_PANELS_DIALOG',
|
|
|
|
});
|
|
|
|
}, [props.dispatch]);
|
|
|
|
|
2024-03-14 21:04:32 +02:00
|
|
|
const closeButton = (
|
|
|
|
<View style={styles.closeButtonContainer}>
|
|
|
|
<IconButton
|
|
|
|
icon='close'
|
|
|
|
accessibilityLabel={_('Close')}
|
2024-03-25 13:39:48 +02:00
|
|
|
onPress={onClose}
|
2024-03-14 21:04:32 +02:00
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Portal>
|
|
|
|
<Modal
|
|
|
|
visible={props.visible}
|
2024-03-25 13:39:48 +02:00
|
|
|
onDismiss={onClose}
|
|
|
|
onRequestClose={onClose}
|
|
|
|
animationType='fade'
|
|
|
|
transparent={true}
|
|
|
|
containerStyle={styles.dialog}
|
2024-03-14 21:04:32 +02:00
|
|
|
>
|
|
|
|
{closeButton}
|
|
|
|
{renderTabContent()}
|
|
|
|
{renderTabSelector()}
|
|
|
|
</Modal>
|
|
|
|
</Portal>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default connect((state: AppState) => {
|
|
|
|
return {
|
|
|
|
themeId: state.settings.theme,
|
|
|
|
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
2024-03-25 13:39:48 +02:00
|
|
|
visible: state.showPanelsDialog,
|
2024-03-14 21:04:32 +02:00
|
|
|
pluginStates: state.pluginService.plugins,
|
|
|
|
};
|
|
|
|
})(PluginPanelViewer);
|