1
0
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:
Henry Heino 2024-03-25 04:39:48 -07:00 committed by GitHub
parent 2de5c1bbf8
commit 40dbb8bd7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 122 additions and 55 deletions

View File

@ -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>

View File

@ -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);

View File

@ -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>
);
}

View File

@ -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/>
</>
);
};

View File

@ -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);

View File

@ -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':
{

View File

@ -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;