2024-03-14 21:04:32 +02:00
|
|
|
|
|
|
|
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
|
|
|
import * as React from 'react';
|
2024-04-25 15:02:10 +02:00
|
|
|
import { Button, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
2024-03-14 21:04:32 +02:00
|
|
|
import useViewInfos from './hooks/useViewInfos';
|
2024-05-02 15:59:50 +02:00
|
|
|
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, 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-04-25 15:02:10 +02:00
|
|
|
import { View, StyleSheet, AccessibilityInfo } from 'react-native';
|
2024-03-14 21:04:32 +02:00
|
|
|
import { _ } from '@joplin/lib/locale';
|
|
|
|
import Setting from '@joplin/lib/models/Setting';
|
2024-03-25 13:39:48 +02:00
|
|
|
import { Dispatch } from 'redux';
|
2024-06-04 10:57:52 +02:00
|
|
|
import DismissibleDialog, { DialogSize } from '../../../components/DismissibleDialog';
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
2024-04-25 15:02:10 +02:00
|
|
|
const styles = StyleSheet.create({
|
|
|
|
webView: {
|
|
|
|
backgroundColor: 'transparent',
|
|
|
|
display: 'flex',
|
|
|
|
},
|
|
|
|
webViewContainer: {
|
|
|
|
flexGrow: 1,
|
|
|
|
flexShrink: 1,
|
|
|
|
},
|
|
|
|
});
|
2024-03-14 21:04:32 +02:00
|
|
|
|
2024-03-25 13:39:48 +02:00
|
|
|
type ButtonInfo = {
|
|
|
|
value: string;
|
|
|
|
label: string;
|
|
|
|
icon: string;
|
|
|
|
};
|
|
|
|
|
2024-05-02 15:59:50 +02:00
|
|
|
const useSelectedTabId = (
|
|
|
|
buttonInfos: ButtonInfo[],
|
|
|
|
viewInfoById: Record<string, ViewInfo>,
|
|
|
|
) => {
|
|
|
|
const viewInfoByIdRef = useRef(viewInfoById);
|
|
|
|
viewInfoByIdRef.current = viewInfoById;
|
|
|
|
|
2024-03-25 13:39:48 +02:00
|
|
|
const getDefaultSelectedTabId = useCallback((): string|undefined => {
|
|
|
|
const lastSelectedId = Setting.value('ui.lastSelectedPluginPanel');
|
2024-05-02 15:59:50 +02:00
|
|
|
const lastSelectedInfo = viewInfoByIdRef.current[lastSelectedId];
|
|
|
|
if (lastSelectedId && lastSelectedInfo && lastSelectedInfo.view.opened) {
|
2024-03-25 13:39:48 +02:00
|
|
|
return lastSelectedId;
|
|
|
|
} else {
|
|
|
|
return buttonInfos[0]?.value;
|
|
|
|
}
|
2024-05-02 15:59:50 +02:00
|
|
|
}, [buttonInfos]);
|
2024-03-25 13:39:48 +02:00
|
|
|
|
|
|
|
const [selectedTabId, setSelectedTabId] = useState<string|undefined>(getDefaultSelectedTabId);
|
|
|
|
|
|
|
|
useEffect(() => {
|
2024-05-02 15:59:50 +02:00
|
|
|
if (!selectedTabId || !viewInfoById[selectedTabId]?.view?.opened) {
|
2024-03-25 13:39:48 +02:00
|
|
|
setSelectedTabId(getDefaultSelectedTabId());
|
|
|
|
}
|
2024-05-02 15:59:50 +02:00
|
|
|
}, [selectedTabId, viewInfoById, getDefaultSelectedTabId]);
|
2024-03-25 13:39:48 +02:00
|
|
|
|
|
|
|
useEffect(() => {
|
2024-05-02 15:59:50 +02:00
|
|
|
if (!selectedTabId) return;
|
2024-03-25 13:39:48 +02:00
|
|
|
|
2024-05-02 15:59:50 +02:00
|
|
|
const info = viewInfoByIdRef.current[selectedTabId];
|
2024-03-25 13:39:48 +02:00
|
|
|
Setting.setValue('ui.lastSelectedPluginPanel', selectedTabId);
|
|
|
|
AccessibilityInfo.announceForAccessibility(_('%s tab opened', getTabLabel(info)));
|
2024-05-02 15:59:50 +02:00
|
|
|
}, [selectedTabId]);
|
2024-03-25 13:39:48 +02:00
|
|
|
|
|
|
|
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) {
|
2024-05-02 15:59:50 +02:00
|
|
|
result[info.view.id] = info;
|
2024-03-14 21:04:32 +02:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}, [viewInfos]);
|
|
|
|
|
|
|
|
const buttonInfos = useMemo(() => {
|
|
|
|
return Object.entries(viewInfoById)
|
|
|
|
.filter(([_id, info]) => info.view.containerType === ContainerType.Panel)
|
2024-05-02 15:59:50 +02:00
|
|
|
.filter(([_id, info]) => info.view.opened)
|
2024-03-14 21:04:32 +02:00
|
|
|
.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]);
|
|
|
|
|
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({
|
2024-03-29 14:40:54 +02:00
|
|
|
type: 'SET_PLUGIN_PANELS_DIALOG_VISIBLE',
|
|
|
|
visible: false,
|
2024-03-25 13:39:48 +02:00
|
|
|
});
|
|
|
|
}, [props.dispatch]);
|
|
|
|
|
2024-03-14 21:04:32 +02:00
|
|
|
return (
|
|
|
|
<Portal>
|
2024-04-25 15:02:10 +02:00
|
|
|
<DismissibleDialog
|
|
|
|
themeId={props.themeId}
|
2024-03-14 21:04:32 +02:00
|
|
|
visible={props.visible}
|
2024-06-04 10:57:52 +02:00
|
|
|
size={DialogSize.Large}
|
2024-03-25 13:39:48 +02:00
|
|
|
onDismiss={onClose}
|
2024-03-14 21:04:32 +02:00
|
|
|
>
|
|
|
|
{renderTabContent()}
|
|
|
|
{renderTabSelector()}
|
2024-04-25 15:02:10 +02:00
|
|
|
</DismissibleDialog>
|
2024-03-14 21:04:32 +02:00
|
|
|
</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);
|