mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Android: Toggle plugin panels using a button in the toolbar (#10180)
This commit is contained in:
parent
2de5c1bbf8
commit
40dbb8bd7f
@ -13,8 +13,12 @@ const ModalElement: React.FC<ModalElementProps> = ({
|
||||
containerStyle,
|
||||
...modalProps
|
||||
}) => {
|
||||
// supportedOrientations: On iOS, this allows the dialog to be shown in non-portrait orientations.
|
||||
return (
|
||||
<Modal {...modalProps}>
|
||||
<Modal
|
||||
supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}
|
||||
{...modalProps}
|
||||
>
|
||||
<View style={[styleSheet.modalContainer, containerStyle ? containerStyle : null]}>
|
||||
{children}
|
||||
</View>
|
||||
|
@ -24,6 +24,8 @@ import FolderPicker from './FolderPicker';
|
||||
import { itemIsInTrash } from '@joplin/lib/services/trash';
|
||||
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
|
||||
// We need this to suppress the useless warning
|
||||
// https://github.com/oblador/react-native-vector-icons/issues/1465
|
||||
@ -69,6 +71,7 @@ interface ScreenHeaderProps {
|
||||
onValueChange?: OnValueChangedListener;
|
||||
mustSelect?: boolean;
|
||||
};
|
||||
plugins: PluginStates;
|
||||
|
||||
dispatch: DispatchCommandType;
|
||||
onUndoButtonPress: OnPressCallback;
|
||||
@ -256,6 +259,10 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
}
|
||||
}
|
||||
|
||||
private pluginPanelToggleButton_press() {
|
||||
this.props.dispatch({ type: 'TOGGLE_PLUGIN_PANELS_DIALOG' });
|
||||
}
|
||||
|
||||
private async duplicateButton_press() {
|
||||
const noteIds = this.props.selectedNoteIds;
|
||||
|
||||
@ -443,6 +450,23 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
);
|
||||
}
|
||||
|
||||
const pluginPanelToggleButton = (styles: any, onPress: OnPressCallback) => {
|
||||
const allPluginViews = Object.values(this.props.plugins).map(plugin => Object.values(plugin.views)).flat();
|
||||
const allPanels = allPluginViews.filter(view => view.containerType === ContainerType.Panel);
|
||||
if (allPanels.length === 0) return null;
|
||||
|
||||
return (
|
||||
<CustomButton
|
||||
onPress={onPress}
|
||||
description={_('Plugin panels')}
|
||||
themeId={themeId}
|
||||
contentStyle={styles.iconButton}
|
||||
>
|
||||
<Icon name="extension-puzzle" style={styles.topIcon} />
|
||||
</CustomButton>
|
||||
);
|
||||
};
|
||||
|
||||
function deleteButton(styles: any, onPress: OnPressCallback, disabled: boolean) {
|
||||
return (
|
||||
<CustomButton
|
||||
@ -623,6 +647,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
const backButtonComp = !showBackButton ? null : backButton(this.styles(), () => this.backButton_press(), backButtonDisabled);
|
||||
const selectAllButtonComp = !showSelectAllButton ? null : selectAllButton(this.styles(), () => this.selectAllButton_press());
|
||||
const searchButtonComp = !showSearchButton ? null : searchButton(this.styles(), () => this.searchButton_press());
|
||||
const pluginPanelsComp = pluginPanelToggleButton(this.styles(), () => this.pluginPanelToggleButton_press());
|
||||
const deleteButtonComp = !selectedFolderInTrash && this.props.noteSelectionEnabled ? deleteButton(this.styles(), () => this.deleteButton_press(), headerItemDisabled) : null;
|
||||
const restoreButtonComp = selectedFolderInTrash && this.props.noteSelectionEnabled ? restoreButton(this.styles(), () => this.restoreButton_press(), headerItemDisabled) : null;
|
||||
const duplicateButtonComp = !selectedFolderInTrash && this.props.noteSelectionEnabled ? duplicateButton(this.styles(), () => this.duplicateButton_press(), headerItemDisabled) : null;
|
||||
@ -667,6 +692,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
this.props.showSaveButton === true,
|
||||
)}
|
||||
{titleComp}
|
||||
{pluginPanelsComp}
|
||||
{selectAllButtonComp}
|
||||
{searchButtonComp}
|
||||
{deleteButtonComp}
|
||||
@ -707,6 +733,7 @@ const ScreenHeader = connect((state: State) => {
|
||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
|
||||
mustUpgradeAppMessage: state.mustUpgradeAppMessage,
|
||||
plugins: state.pluginService.plugins,
|
||||
};
|
||||
})(ScreenHeaderComponent);
|
||||
|
||||
|
@ -60,8 +60,6 @@ import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
import { getDisplayParentTitle } from '@joplin/lib/services/trash';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import pickDocument from '../../utils/pickDocument';
|
||||
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import PluginPanelViewer from '../../plugins/PluginRunner/dialogs/PluginPanelViewer';
|
||||
import debounce from '../../utils/debounce';
|
||||
const urlUtils = require('@joplin/lib/urlUtils');
|
||||
|
||||
@ -104,7 +102,6 @@ interface State {
|
||||
imageEditorResourceFilepath: string;
|
||||
noteResources: Record<string, ResourceEntity>;
|
||||
newAndNoTitleChangeNoteId: boolean|null;
|
||||
pluginPanelsVisible: boolean;
|
||||
|
||||
HACK_webviewLoadingState: number;
|
||||
|
||||
@ -163,7 +160,6 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
noteResources: {},
|
||||
imageEditorResourceFilepath: null,
|
||||
newAndNoTitleChangeNoteId: null,
|
||||
pluginPanelsVisible: false,
|
||||
|
||||
// HACK: For reasons I can't explain, when the WebView is present, the TextInput initially does not display (It's just a white rectangle with
|
||||
// no visible text). It will only appear when tapping it or doing certain action like selecting text on the webview. The bug started to
|
||||
@ -1302,18 +1298,6 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
// Only show the plugin panel toggle if any plugins have panels
|
||||
const allPluginViews = Object.values(this.props.plugins).map(plugin => Object.values(plugin.views)).flat();
|
||||
const allPanels = allPluginViews.filter(view => view.containerType === ContainerType.Panel);
|
||||
if (allPanels.length > 0) {
|
||||
output.push({
|
||||
title: _('Show plugin panels'),
|
||||
onPress: () => {
|
||||
this.setState({ pluginPanelsVisible: true });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.menuOptionsCache_ = {};
|
||||
this.menuOptionsCache_[cacheKey] = output;
|
||||
|
||||
@ -1626,10 +1610,6 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
||||
}}
|
||||
/>
|
||||
{noteTagDialog}
|
||||
<PluginPanelViewer
|
||||
visible={this.state.pluginPanelsVisible}
|
||||
onClose={() => this.setState({ pluginPanelsVisible: false })}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ 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;
|
||||
@ -53,6 +54,7 @@ const PluginDialogManager: React.FC<Props> = props => {
|
||||
return (
|
||||
<>
|
||||
{dialogs}
|
||||
<PluginPanelViewer/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,19 +1,21 @@
|
||||
|
||||
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||
import * as React from 'react';
|
||||
import { Button, IconButton, Modal, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
||||
import { Button, IconButton, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
||||
import useViewInfos from './hooks/useViewInfos';
|
||||
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, 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, useWindowDimensions, StyleSheet } from 'react-native';
|
||||
import { View, useWindowDimensions, StyleSheet, AccessibilityInfo } from 'react-native';
|
||||
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';
|
||||
import { Dispatch } from 'redux';
|
||||
import Modal from '../../../components/Modal';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@ -21,7 +23,7 @@ interface Props {
|
||||
pluginHtmlContents: PluginHtmlContents;
|
||||
pluginStates: PluginStates;
|
||||
visible: boolean;
|
||||
onClose: ()=> void;
|
||||
dispatch: Dispatch;
|
||||
}
|
||||
|
||||
|
||||
@ -60,8 +62,63 @@ const useStyles = (themeId: number) => {
|
||||
}, [themeId, windowSize.width, windowSize.height]);
|
||||
};
|
||||
|
||||
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 };
|
||||
};
|
||||
|
||||
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(() => {
|
||||
@ -76,40 +133,16 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
return Object.entries(viewInfoById)
|
||||
.filter(([_id, info]) => info.view.containerType === ContainerType.Panel)
|
||||
.map(([id, info]) => {
|
||||
const pluginName = PluginService.instance().pluginById(info.plugin.id).manifest.name;
|
||||
return {
|
||||
value: id,
|
||||
label: pluginName,
|
||||
label: getTabLabel(info),
|
||||
icon: 'puzzle',
|
||||
};
|
||||
});
|
||||
}, [viewInfoById]);
|
||||
|
||||
const [selectedTabId, setSelectedTabId] = useState(() => {
|
||||
const lastSelectedId = Setting.value('ui.lastSelectedPluginPanel');
|
||||
if (lastSelectedId && viewInfoById[lastSelectedId]) {
|
||||
return lastSelectedId;
|
||||
} else {
|
||||
return buttonInfos[0]?.value;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTabId) return () => {};
|
||||
|
||||
const info = viewInfoById[selectedTabId];
|
||||
const plugin = PluginService.instance().pluginById(info.plugin.id);
|
||||
const controller = plugin.viewController(info.view.id) as WebviewController;
|
||||
controller.setIsShownInModal(true);
|
||||
Setting.setValue('ui.lastSelectedPluginPanel', selectedTabId);
|
||||
|
||||
return () => {
|
||||
controller.setIsShownInModal(false);
|
||||
};
|
||||
}, [viewInfoById, selectedTabId]);
|
||||
|
||||
|
||||
const styles = useStyles(props.themeId);
|
||||
const { selectedTabId, setSelectedTabId } = useSelectedTabId(buttonInfos, viewInfoById);
|
||||
|
||||
const viewInfo = viewInfoById[selectedTabId];
|
||||
|
||||
@ -138,7 +171,11 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
// As such, we include a special case:
|
||||
if (buttonInfos.length === 1) {
|
||||
const buttonInfo = buttonInfos[0];
|
||||
return <Button icon={buttonInfo.icon}>{buttonInfo.label}</Button>;
|
||||
return (
|
||||
<Button icon={buttonInfo.icon} onPress={()=>setSelectedTabId(buttonInfo.value)}>
|
||||
{buttonInfo.label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -150,12 +187,18 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
props.dispatch({
|
||||
type: 'TOGGLE_PLUGIN_PANELS_DIALOG',
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const closeButton = (
|
||||
<View style={styles.closeButtonContainer}>
|
||||
<IconButton
|
||||
icon='close'
|
||||
accessibilityLabel={_('Close')}
|
||||
onPress={props.onClose}
|
||||
onPress={onClose}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@ -164,8 +207,11 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={props.visible}
|
||||
onDismiss={props.onClose}
|
||||
contentContainerStyle={styles.dialog}
|
||||
onDismiss={onClose}
|
||||
onRequestClose={onClose}
|
||||
animationType='fade'
|
||||
transparent={true}
|
||||
containerStyle={styles.dialog}
|
||||
>
|
||||
{closeButton}
|
||||
{renderTabContent()}
|
||||
@ -179,6 +225,7 @@ export default connect((state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
visible: state.showPanelsDialog,
|
||||
pluginStates: state.pluginService.plugins,
|
||||
};
|
||||
})(PluginPanelViewer);
|
||||
|
@ -259,6 +259,7 @@ const appDefaultState: AppState = { ...defaultState, sideMenuOpenPercent: 0,
|
||||
noteSideMenuOptions: null,
|
||||
isOnMobileData: false,
|
||||
disableSideMenuGestures: false,
|
||||
showPanelsDialog: false,
|
||||
};
|
||||
|
||||
const appReducer = (state = appDefaultState, action: any) => {
|
||||
@ -379,6 +380,11 @@ const appReducer = (state = appDefaultState, action: any) => {
|
||||
newState.sideMenuOpenPercent = action.value;
|
||||
break;
|
||||
|
||||
case 'TOGGLE_PLUGIN_PANELS_DIALOG':
|
||||
newState = { ...state };
|
||||
newState.showPanelsDialog = !newState.showPanelsDialog;
|
||||
break;
|
||||
|
||||
case 'NOTE_SELECTION_TOGGLE':
|
||||
|
||||
{
|
||||
|
@ -2,6 +2,7 @@ import { State } from '@joplin/lib/reducer';
|
||||
|
||||
export interface AppState extends State {
|
||||
sideMenuOpenPercent: number;
|
||||
showPanelsDialog: boolean;
|
||||
isOnMobileData: boolean;
|
||||
route: any;
|
||||
smartFilterId: string;
|
||||
|
Loading…
Reference in New Issue
Block a user