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:
parent
4056fc2281
commit
85f890e7c5
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user