You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-07-16 00:14:34 +02:00
Mobile: Plugins: Make panel opening/closing more consistent with desktop (#10385)
This commit is contained in:
@ -34,12 +34,17 @@ const PADDING_V = 10;
|
|||||||
type OnSelectCallbackType=()=> void;
|
type OnSelectCallbackType=()=> void;
|
||||||
type OnPressCallback=()=> void;
|
type OnPressCallback=()=> void;
|
||||||
|
|
||||||
export interface MenuOptionType {
|
export type MenuOptionType = {
|
||||||
onPress: OnPressCallback;
|
onPress: OnPressCallback;
|
||||||
isDivider?: boolean;
|
isDivider?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}|{
|
||||||
|
isDivider: true;
|
||||||
|
title?: undefined;
|
||||||
|
onPress?: undefined;
|
||||||
|
disabled?: false;
|
||||||
|
};
|
||||||
|
|
||||||
interface ScreenHeaderProps {
|
interface ScreenHeaderProps {
|
||||||
selectedNoteIds: string[];
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
const pluginPanelToggleButton = (styles: any, onPress: OnPressCallback) => {
|
const pluginPanelToggleButton = (styles: any, onPress: OnPressCallback) => {
|
||||||
const allPluginViews = Object.values(this.props.plugins).map(plugin => Object.values(plugin.views)).flat();
|
const allPluginViews = Object.values(this.props.plugins).map(plugin => Object.values(plugin.views)).flat();
|
||||||
const allPanels = allPluginViews.filter(view => view.containerType === ContainerType.Panel);
|
const allVisiblePanels = allPluginViews.filter(
|
||||||
if (allPanels.length === 0) return null;
|
view => view.containerType === ContainerType.Panel && view.opened,
|
||||||
|
);
|
||||||
|
if (allVisiblePanels.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomButton
|
<CustomButton
|
||||||
|
@ -58,10 +58,11 @@ import { SelectionRange } from '../NoteEditor/types';
|
|||||||
import { AppState } from '../../utils/types';
|
import { AppState } from '../../utils/types';
|
||||||
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||||
import { getDisplayParentTitle } from '@joplin/lib/services/trash';
|
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 pickDocument from '../../utils/pickDocument';
|
||||||
import debounce from '../../utils/debounce';
|
import debounce from '../../utils/debounce';
|
||||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
const urlUtils = require('@joplin/lib/urlUtils');
|
const urlUtils = require('@joplin/lib/urlUtils');
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// 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 readOnly = this.state.readOnly;
|
||||||
const isDeleted = !!this.state.note.deleted_time;
|
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_) this.menuOptionsCache_ = {};
|
||||||
|
|
||||||
if (this.menuOptionsCache_[cacheKey]) return this.menuOptionsCache_[cacheKey];
|
if (this.menuOptionsCache_[cacheKey]) return this.menuOptionsCache_[cacheKey];
|
||||||
@ -1321,6 +1324,25 @@ class NoteScreenComponent extends BaseScreenComponent<Props, State> implements B
|
|||||||
disabled: readOnly,
|
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_ = {};
|
||||||
this.menuOptionsCache_[cacheKey] = output;
|
this.menuOptionsCache_[cacheKey] = output;
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@ import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Button, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
import { Button, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
||||||
import useViewInfos from './hooks/useViewInfos';
|
import useViewInfos from './hooks/useViewInfos';
|
||||||
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
import { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { AppState } from '../../../utils/types';
|
import { AppState } from '../../../utils/types';
|
||||||
@ -42,43 +42,38 @@ type ButtonInfo = {
|
|||||||
icon: string;
|
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 getDefaultSelectedTabId = useCallback((): string|undefined => {
|
||||||
const lastSelectedId = Setting.value('ui.lastSelectedPluginPanel');
|
const lastSelectedId = Setting.value('ui.lastSelectedPluginPanel');
|
||||||
if (lastSelectedId && viewInfoById[lastSelectedId]) {
|
const lastSelectedInfo = viewInfoByIdRef.current[lastSelectedId];
|
||||||
|
if (lastSelectedId && lastSelectedInfo && lastSelectedInfo.view.opened) {
|
||||||
return lastSelectedId;
|
return lastSelectedId;
|
||||||
} else {
|
} else {
|
||||||
return buttonInfos[0]?.value;
|
return buttonInfos[0]?.value;
|
||||||
}
|
}
|
||||||
}, [buttonInfos, viewInfoById]);
|
}, [buttonInfos]);
|
||||||
|
|
||||||
const [selectedTabId, setSelectedTabId] = useState<string|undefined>(getDefaultSelectedTabId);
|
const [selectedTabId, setSelectedTabId] = useState<string|undefined>(getDefaultSelectedTabId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedTabId) {
|
if (!selectedTabId || !viewInfoById[selectedTabId]?.view?.opened) {
|
||||||
setSelectedTabId(getDefaultSelectedTabId());
|
setSelectedTabId(getDefaultSelectedTabId());
|
||||||
}
|
}
|
||||||
}, [selectedTabId, getDefaultSelectedTabId]);
|
}, [selectedTabId, viewInfoById, getDefaultSelectedTabId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedTabId) return () => {};
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const info = viewInfoByIdRef.current[selectedTabId];
|
||||||
Setting.setValue('ui.lastSelectedPluginPanel', selectedTabId);
|
Setting.setValue('ui.lastSelectedPluginPanel', selectedTabId);
|
||||||
AccessibilityInfo.announceForAccessibility(_('%s tab opened', getTabLabel(info)));
|
AccessibilityInfo.announceForAccessibility(_('%s tab opened', getTabLabel(info)));
|
||||||
|
}, [selectedTabId]);
|
||||||
return () => {
|
|
||||||
controller?.setIsShownInModal(false);
|
|
||||||
};
|
|
||||||
}, [viewInfoById, selectedTabId]);
|
|
||||||
|
|
||||||
return { setSelectedTabId, selectedTabId };
|
return { setSelectedTabId, selectedTabId };
|
||||||
};
|
};
|
||||||
@ -98,7 +93,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
|||||||
const viewInfoById = useMemo(() => {
|
const viewInfoById = useMemo(() => {
|
||||||
const result: Record<string, ViewInfo> = {};
|
const result: Record<string, ViewInfo> = {};
|
||||||
for (const info of viewInfos) {
|
for (const info of viewInfos) {
|
||||||
result[`${info.plugin.id}--${info.view.id}`] = info;
|
result[info.view.id] = info;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [viewInfos]);
|
}, [viewInfos]);
|
||||||
@ -106,6 +101,7 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
|||||||
const buttonInfos = useMemo(() => {
|
const buttonInfos = useMemo(() => {
|
||||||
return Object.entries(viewInfoById)
|
return Object.entries(viewInfoById)
|
||||||
.filter(([_id, info]) => info.view.containerType === ContainerType.Panel)
|
.filter(([_id, info]) => info.view.containerType === ContainerType.Panel)
|
||||||
|
.filter(([_id, info]) => info.view.opened)
|
||||||
.map(([id, info]) => {
|
.map(([id, info]) => {
|
||||||
return {
|
return {
|
||||||
value: id,
|
value: id,
|
||||||
|
@ -3,6 +3,7 @@ import shim from '../../shim';
|
|||||||
import { ButtonSpec, DialogResult, ViewHandle } from './api/types';
|
import { ButtonSpec, DialogResult, ViewHandle } from './api/types';
|
||||||
const { toSystemSlashes } = require('../../path-utils');
|
const { toSystemSlashes } = require('../../path-utils');
|
||||||
import PostMessageService, { MessageParticipant } from '../PostMessageService';
|
import PostMessageService, { MessageParticipant } from '../PostMessageService';
|
||||||
|
import { PluginViewState } from './reducer';
|
||||||
|
|
||||||
export enum ContainerType {
|
export enum ContainerType {
|
||||||
Panel = 'panel',
|
Panel = 'panel',
|
||||||
@ -49,27 +50,28 @@ export default class WebviewController extends ViewController {
|
|||||||
private messageListener_: Function = null;
|
private messageListener_: Function = null;
|
||||||
private closeResponse_: CloseResponse = 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
|
// 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) {
|
public constructor(handle: ViewHandle, pluginId: string, store: any, baseDir: string, containerType: ContainerType) {
|
||||||
super(handle, pluginId, store);
|
super(handle, pluginId, store);
|
||||||
this.baseDir_ = toSystemSlashes(baseDir, 'linux');
|
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({
|
this.store.dispatch({
|
||||||
type: 'PLUGIN_VIEW_ADD',
|
type: 'PLUGIN_VIEW_ADD',
|
||||||
pluginId: pluginId,
|
pluginId: pluginId,
|
||||||
view: {
|
view,
|
||||||
id: this.handle,
|
|
||||||
type: this.type,
|
|
||||||
containerType: containerType,
|
|
||||||
html: '',
|
|
||||||
scripts: [],
|
|
||||||
opened: false,
|
|
||||||
buttons: null,
|
|
||||||
fitToContent: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,39 +149,36 @@ export default class WebviewController extends ViewController {
|
|||||||
// Specific to panels
|
// Specific to panels
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
|
private showWithAppLayout() {
|
||||||
|
return this.containerType === ContainerType.Panel && !!this.store.getState().mainLayout;
|
||||||
|
}
|
||||||
|
|
||||||
public async show(show = true): Promise<void> {
|
public async show(show = true): Promise<void> {
|
||||||
this.store.dispatch({
|
if (this.showWithAppLayout()) {
|
||||||
type: 'MAIN_LAYOUT_SET_ITEM_PROP',
|
this.store.dispatch({
|
||||||
itemKey: this.handle,
|
type: 'MAIN_LAYOUT_SET_ITEM_PROP',
|
||||||
propName: 'visible',
|
itemKey: this.handle,
|
||||||
propValue: show,
|
propName: 'visible',
|
||||||
});
|
propValue: show,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setStoreProp('opened', show);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async hide(): Promise<void> {
|
public async hide(): Promise<void> {
|
||||||
return this.show(false);
|
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 {
|
public get visible(): boolean {
|
||||||
const appState = this.store.getState();
|
const appState = this.store.getState();
|
||||||
|
|
||||||
if (this.panelInModalMode_) {
|
// Mobile: There is no appState.mainLayout
|
||||||
return true;
|
if (!this.showWithAppLayout()) {
|
||||||
|
return this.storeView.opened;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainLayout = appState.mainLayout;
|
const mainLayout = appState.mainLayout;
|
||||||
|
|
||||||
// Mobile: There is no appState.mainLayout
|
|
||||||
if (!mainLayout) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = findItemByKey(mainLayout, this.handle);
|
const item = findItemByKey(mainLayout, this.handle);
|
||||||
return item ? item.visible : false;
|
return item ? item.visible : false;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user