1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Mobile: Plugins: Make panel opening/closing more consistent with desktop (#10385)

This commit is contained in:
Henry Heino 2024-05-02 06:59:50 -07:00 committed by GitHub
parent 4056fc2281
commit 85f890e7c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 86 additions and 62 deletions

View File

@ -34,12 +34,17 @@ const PADDING_V = 10;
type OnSelectCallbackType=()=> void;
type OnPressCallback=()=> void;
export interface MenuOptionType {
export type MenuOptionType = {
onPress: OnPressCallback;
isDivider?: boolean;
title: string;
disabled?: boolean;
}
}|{
isDivider: true;
title?: undefined;
onPress?: undefined;
disabled?: false;
};
interface ScreenHeaderProps {
selectedNoteIds: string[];
@ -427,8 +432,10 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
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;
const allVisiblePanels = allPluginViews.filter(
view => view.containerType === ContainerType.Panel && view.opened,
);
if (allVisiblePanels.length === 0) return null;
return (
<CustomButton

View File

@ -58,10 +58,11 @@ import { SelectionRange } from '../NoteEditor/types';
import { AppState } from '../../utils/types';
import restoreItems from '@joplin/lib/services/trash/restoreItems';
import { getDisplayParentTitle } from '@joplin/lib/services/trash';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import pickDocument from '../../utils/pickDocument';
import debounce from '../../utils/debounce';
import { focus } from '@joplin/lib/utils/focusHandler';
import CommandService from '@joplin/lib/services/CommandService';
const urlUtils = require('@joplin/lib/urlUtils');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@ -1207,7 +1208,9 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
const readOnly = this.state.readOnly;
const isDeleted = !!this.state.note.deleted_time;
const cacheKey = md5([isTodo, isSaved].join('_'));
const pluginCommands = pluginUtils.commandNamesFromViews(this.props.plugins, 'noteToolbar');
const cacheKey = md5([isTodo, isSaved, pluginCommands.join(',')].join('_'));
if (!this.menuOptionsCache_) this.menuOptionsCache_ = {};
if (this.menuOptionsCache_[cacheKey]) return this.menuOptionsCache_[cacheKey];
@ -1321,6 +1324,25 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
disabled: readOnly,
});
if (pluginCommands.length) {
output.push({ isDivider: true });
const commandService = CommandService.instance();
for (const commandName of pluginCommands) {
if (commandName === '-') {
output.push({ isDivider: true });
} else {
output.push({
title: commandService.description(commandName),
onPress: async () => {
void commandService.execute(commandName);
},
disabled: !commandService.isEnabled(commandName),
});
}
}
}
this.menuOptionsCache_ = {};
this.menuOptionsCache_[cacheKey] = output;

View File

@ -3,8 +3,8 @@ import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services
import * as React from 'react';
import { Button, Portal, SegmentedButtons, Text } from 'react-native-paper';
import useViewInfos from './hooks/useViewInfos';
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import PluginService from '@joplin/lib/services/plugins/PluginService';
import { connect } from 'react-redux';
import { AppState } from '../../../utils/types';
@ -42,43 +42,38 @@ type ButtonInfo = {
icon: string;
};
const useSelectedTabId = (buttonInfos: ButtonInfo[], viewInfoById: Record<string, ViewInfo>) => {
const useSelectedTabId = (
buttonInfos: ButtonInfo[],
viewInfoById: Record<string, ViewInfo>,
) => {
const viewInfoByIdRef = useRef(viewInfoById);
viewInfoByIdRef.current = viewInfoById;
const getDefaultSelectedTabId = useCallback((): string|undefined => {
const lastSelectedId = Setting.value('ui.lastSelectedPluginPanel');
if (lastSelectedId && viewInfoById[lastSelectedId]) {
const lastSelectedInfo = viewInfoByIdRef.current[lastSelectedId];
if (lastSelectedId && lastSelectedInfo && lastSelectedInfo.view.opened) {
return lastSelectedId;
} else {
return buttonInfos[0]?.value;
}
}, [buttonInfos, viewInfoById]);
}, [buttonInfos]);
const [selectedTabId, setSelectedTabId] = useState<string|undefined>(getDefaultSelectedTabId);
useEffect(() => {
if (!selectedTabId) {
if (!selectedTabId || !viewInfoById[selectedTabId]?.view?.opened) {
setSelectedTabId(getDefaultSelectedTabId());
}
}, [selectedTabId, getDefaultSelectedTabId]);
}, [selectedTabId, viewInfoById, 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);
}
if (!selectedTabId) return;
const info = viewInfoByIdRef.current[selectedTabId];
Setting.setValue('ui.lastSelectedPluginPanel', selectedTabId);
AccessibilityInfo.announceForAccessibility(_('%s tab opened', getTabLabel(info)));
return () => {
controller?.setIsShownInModal(false);
};
}, [viewInfoById, selectedTabId]);
}, [selectedTabId]);
return { setSelectedTabId, selectedTabId };
};
@ -98,7 +93,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
const viewInfoById = useMemo(() => {
const result: Record<string, ViewInfo> = {};
for (const info of viewInfos) {
result[`${info.plugin.id}--${info.view.id}`] = info;
result[info.view.id] = info;
}
return result;
}, [viewInfos]);
@ -106,6 +101,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
const buttonInfos = useMemo(() => {
return Object.entries(viewInfoById)
.filter(([_id, info]) => info.view.containerType === ContainerType.Panel)
.filter(([_id, info]) => info.view.opened)
.map(([id, info]) => {
return {
value: id,

View File

@ -3,6 +3,7 @@ import shim from '../../shim';
import { ButtonSpec, DialogResult, ViewHandle } from './api/types';
const { toSystemSlashes } = require('../../path-utils');
import PostMessageService, { MessageParticipant } from '../PostMessageService';
import { PluginViewState } from './reducer';
export enum ContainerType {
Panel = 'panel',
@ -49,27 +50,28 @@ export default class WebviewController extends ViewController {
private messageListener_: Function = null;
private closeResponse_: CloseResponse = null;
// True if a **panel** is shown in a modal window.
private panelInModalMode_ = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public constructor(handle: ViewHandle, pluginId: string, store: any, baseDir: string, containerType: ContainerType) {
super(handle, pluginId, store);
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
const view: PluginViewState = {
id: this.handle,
type: this.type,
containerType: containerType,
html: '',
scripts: [],
// Opened is used for dialogs and mobile panels (which are shown
// like dialogs):
opened: containerType === ContainerType.Panel,
buttons: null,
fitToContent: true,
};
this.store.dispatch({
type: 'PLUGIN_VIEW_ADD',
pluginId: pluginId,
view: {
id: this.handle,
type: this.type,
containerType: containerType,
html: '',
scripts: [],
opened: false,
buttons: null,
fitToContent: true,
},
view,
});
}
@ -147,39 +149,36 @@ export default class WebviewController extends ViewController {
// Specific to panels
// ---------------------------------------------
private showWithAppLayout() {
return this.containerType === ContainerType.Panel && !!this.store.getState().mainLayout;
}
public async show(show = true): Promise<void> {
this.store.dispatch({
type: 'MAIN_LAYOUT_SET_ITEM_PROP',
itemKey: this.handle,
propName: 'visible',
propValue: show,
});
if (this.showWithAppLayout()) {
this.store.dispatch({
type: 'MAIN_LAYOUT_SET_ITEM_PROP',
itemKey: this.handle,
propName: 'visible',
propValue: show,
});
} else {
this.setStoreProp('opened', show);
}
}
public async hide(): Promise<void> {
return this.show(false);
}
// This method allows us to determine whether a panel is shown in dialog mode,
// which is used on mobile.
public setIsShownInModal(shown: boolean) {
this.panelInModalMode_ = shown;
}
public get visible(): boolean {
const appState = this.store.getState();
if (this.panelInModalMode_) {
return true;
// Mobile: There is no appState.mainLayout
if (!this.showWithAppLayout()) {
return this.storeView.opened;
}
const mainLayout = appState.mainLayout;
// Mobile: There is no appState.mainLayout
if (!mainLayout) {
return false;
}
const item = findItemByKey(mainLayout, this.handle);
return item ? item.visible : false;
}