mirror of
https://github.com/laurent22/joplin.git
synced 2026-10-25 02:47:28 +00:00
Mobile: Implement redesigned notebook long-press menu (#16027)
This commit is contained in:
1 parent
d7c29c1d1a
commit
1d7c5c0a44
10 files changed
+241
-170
No files matched your search
@@ -700,6 +700,7 @@ packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/BottomDrawer.js
|
||||
packages/app-mobile/components/BottomDrawerMenu.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
|
||||
@@ -726,6 +726,7 @@ packages/app-mobile/commands/util/goToNote.js
|
||||
packages/app-mobile/commands/util/showResource.js
|
||||
packages/app-mobile/components/BetaChip.js
|
||||
packages/app-mobile/components/BottomDrawer.js
|
||||
packages/app-mobile/components/BottomDrawerMenu.js
|
||||
packages/app-mobile/components/CameraView/ActionButtons.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.jest.js
|
||||
packages/app-mobile/components/CameraView/Camera/index.js
|
||||
|
||||
@@ -9,9 +9,15 @@ import { AppState } from '../utils/types';
|
||||
import useReduceMotionEnabled from '../utils/hooks/useReduceMotionEnabled';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
export enum MenuAlignment {
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
style: ViewStyle;
|
||||
alignment: MenuAlignment;
|
||||
children: React.ReactNode;
|
||||
visible: boolean;
|
||||
draggable: boolean;
|
||||
@@ -20,6 +26,7 @@ interface Props {
|
||||
}
|
||||
|
||||
interface UseStylesProps {
|
||||
alignment: MenuAlignment;
|
||||
theme: ThemeStyle;
|
||||
dragging: boolean;
|
||||
draggable: boolean;
|
||||
@@ -27,7 +34,7 @@ interface UseStylesProps {
|
||||
dragOffset: Animated.AnimatedInterpolation<number>;
|
||||
}
|
||||
|
||||
const useStyles = ({ theme, dragging, draggable, dragOffset, backgroundOpacity }: UseStylesProps) => {
|
||||
const useStyles = ({ theme, dragging, alignment, draggable, dragOffset, backgroundOpacity }: UseStylesProps) => {
|
||||
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
|
||||
const safeAreaPadding = useSafeAreaPadding();
|
||||
|
||||
@@ -61,7 +68,7 @@ const useStyles = ({ theme, dragging, draggable, dragOffset, backgroundOpacity }
|
||||
},
|
||||
menuStyle: {
|
||||
zIndex: 1,
|
||||
alignSelf: 'flex-end',
|
||||
alignSelf: alignment === MenuAlignment.Center ? 'center' : 'flex-end',
|
||||
...(isSmallWidthScreen ? {
|
||||
// Center on small screens, rather than float right.
|
||||
alignSelf: 'center',
|
||||
@@ -153,7 +160,7 @@ const useStyles = ({ theme, dragging, draggable, dragOffset, backgroundOpacity }
|
||||
zIndex: 2,
|
||||
},
|
||||
});
|
||||
}, [theme, safeAreaPadding, windowWidth, dragging, draggable, dragOffset, windowHeight, backgroundOpacity, menuMarginTop]);
|
||||
}, [theme, safeAreaPadding, windowWidth, dragging, draggable, alignment, dragOffset, windowHeight, backgroundOpacity, menuMarginTop]);
|
||||
};
|
||||
|
||||
interface UsePanResponderProps {
|
||||
@@ -206,6 +213,13 @@ const usePanResponder = ({
|
||||
[onStartEvent]: (event: GestureResponderEvent) => {
|
||||
return isInDragHandle(event.nativeEvent.pageY);
|
||||
},
|
||||
|
||||
// On Android, we need to set the pan responder immediately to allow dragging the menu on non-Pressable elements
|
||||
...(Platform.OS === 'android' ? {
|
||||
onStartShouldSetPanResponder: () => isScrolledToTopRef.current,
|
||||
onShouldBlockNativeResponder: event => isInDragHandle(event.nativeEvent.pageY),
|
||||
} : {}),
|
||||
|
||||
[onMoveEvent]: (_event: GestureResponderEvent, gestureState: PanResponderGestureState) => {
|
||||
if (!isScrolledToTopRef.current) {
|
||||
return false;
|
||||
@@ -313,7 +327,7 @@ const BottomDrawer: React.FC<Props> = props => {
|
||||
const [animating, setAnimating] = useState(false);
|
||||
const menuYOffset = useMemo(() => menuDragOffset, [menuDragOffset]);
|
||||
const styles = useStyles({
|
||||
theme, dragging, draggable: props.draggable, dragOffset: menuYOffset, backgroundOpacity,
|
||||
theme, dragging, alignment: props.alignment, draggable: props.draggable, dragOffset: menuYOffset, backgroundOpacity,
|
||||
});
|
||||
|
||||
const reduceMotionEnabled = useReduceMotionEnabled();
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { StyleSheet, View, useWindowDimensions, TextStyle } from 'react-native';
|
||||
import { themeStyle } from './global-style';
|
||||
import BottomDrawer, { MenuAlignment } from './BottomDrawer';
|
||||
import { TouchableRipple, Text } from 'react-native-paper';
|
||||
import Icon from './Icon';
|
||||
|
||||
interface MenuOptionDivider {
|
||||
isDivider: true;
|
||||
}
|
||||
|
||||
export enum MenuOptionStyle {
|
||||
Normal = 'normal',
|
||||
Destructive = 'destructive',
|
||||
}
|
||||
|
||||
export interface MenuOptionButton {
|
||||
key?: string;
|
||||
isDivider?: false|undefined;
|
||||
disabled?: boolean;
|
||||
style?: MenuOptionStyle;
|
||||
onPress: ()=> void;
|
||||
icon?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type MenuOption = MenuOptionDivider|MenuOptionButton;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
visible: boolean;
|
||||
alignment: MenuAlignment;
|
||||
title?: string;
|
||||
onDismiss: ()=> void;
|
||||
options: MenuOption[];
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number) => {
|
||||
const windowWidth = useWindowDimensions().width;
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
const dividerMargin = theme.marginSmall;
|
||||
|
||||
return StyleSheet.create({
|
||||
title: {
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
// Match the space from button text to a neighbouring divider:
|
||||
paddingVertical: theme.marginMedium + dividerMargin,
|
||||
marginBottom: theme.marginSmall,
|
||||
|
||||
lineHeight: theme.fontSizeLarger,
|
||||
fontSize: theme.fontSizeLarger,
|
||||
|
||||
borderColor: theme.dividerColor,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
menu: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
menuContent: { flexDirection: 'column', width: '100%' },
|
||||
menuItem: {
|
||||
alignItems: 'flex-start',
|
||||
padding: theme.marginMedium,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
minWidth: Math.min(350, windowWidth),
|
||||
},
|
||||
menuItemContent: {
|
||||
flexDirection: 'row',
|
||||
gap: theme.marginSmall,
|
||||
alignItems: 'center',
|
||||
},
|
||||
menuItemText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
menuItemTextDisabled: {
|
||||
opacity: theme.disabledOpacity,
|
||||
},
|
||||
menuItemTextDestructive: {
|
||||
color: theme.destructiveColor,
|
||||
},
|
||||
divider: {
|
||||
height: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: theme.dividerColor,
|
||||
marginVertical: dividerMargin,
|
||||
marginHorizontal: theme.margin,
|
||||
},
|
||||
});
|
||||
}, [themeId, windowWidth]);
|
||||
};
|
||||
|
||||
const BottomDrawerMenu: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId);
|
||||
|
||||
const menuOptionComponents: React.ReactNode[] = [];
|
||||
|
||||
let keyCounter = 0;
|
||||
for (const option of props.options) {
|
||||
if (option.isDivider === true) {
|
||||
menuOptionComponents.push(
|
||||
<View key={`menuOption_divider_${keyCounter++}`} style={styles.divider} />,
|
||||
);
|
||||
} else {
|
||||
const key = `menuOption_${option.key ?? keyCounter++}`;
|
||||
const textStyles: TextStyle[] = [styles.menuItemText];
|
||||
if (option.disabled) {
|
||||
textStyles.push(styles.menuItemTextDisabled);
|
||||
}
|
||||
if (option.style === MenuOptionStyle.Destructive) {
|
||||
textStyles.push(styles.menuItemTextDestructive);
|
||||
}
|
||||
|
||||
menuOptionComponents.push(
|
||||
<TouchableRipple
|
||||
borderless={true}
|
||||
role='button'
|
||||
style={styles.menuItem}
|
||||
onPress={() => {
|
||||
option.onPress();
|
||||
props.onDismiss();
|
||||
}}
|
||||
key={key}
|
||||
disabled={!!option.disabled}
|
||||
>
|
||||
<View style={styles.menuItemContent}>
|
||||
{option.icon && <Icon name={option.icon} style={textStyles} accessibilityLabel={null} />}
|
||||
<Text
|
||||
style={textStyles}
|
||||
disabled={option.disabled}
|
||||
>{option.title}</Text>
|
||||
</View>
|
||||
</TouchableRipple>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const titleComponent = props.title ? <>
|
||||
<Text
|
||||
role='heading'
|
||||
variant='titleMedium'
|
||||
style={styles.title}
|
||||
>{props.title}</Text>
|
||||
</> : null;
|
||||
return <BottomDrawer
|
||||
visible={props.visible}
|
||||
onDismiss={props.onDismiss}
|
||||
alignment={props.alignment}
|
||||
draggable={true}
|
||||
style={styles.menu}
|
||||
>
|
||||
<View
|
||||
style={styles.menuContent}
|
||||
testID={`menu-content-${props.visible ? 'open' : 'closed'}`}
|
||||
>
|
||||
{titleComponent}
|
||||
{menuOptionComponents}
|
||||
</View>
|
||||
</BottomDrawer>;
|
||||
};
|
||||
|
||||
export default BottomDrawerMenu;
|
||||
@@ -1,129 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { StyleSheet, View, Text, useWindowDimensions, TextStyle } from 'react-native';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { themeStyle } from '../global-style';
|
||||
import BottomDrawer from '../BottomDrawer';
|
||||
import { TouchableRipple } from 'react-native-paper';
|
||||
import Icon from '../Icon';
|
||||
|
||||
interface MenuOptionDivider {
|
||||
isDivider: true;
|
||||
}
|
||||
|
||||
export enum MenuOptionStyle {
|
||||
Normal = 'normal',
|
||||
Destructive = 'destructive',
|
||||
}
|
||||
|
||||
export interface MenuOptionButton {
|
||||
key?: string;
|
||||
isDivider?: false|undefined;
|
||||
disabled?: boolean;
|
||||
style?: MenuOptionStyle;
|
||||
onPress: ()=> void;
|
||||
icon?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type MenuOptionType = MenuOptionDivider|MenuOptionButton;
|
||||
import BottomDrawerMenu, { MenuOption } from '../BottomDrawerMenu';
|
||||
import { MenuAlignment } from '../BottomDrawer';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
options: MenuOptionType[];
|
||||
options: MenuOption[];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number) => {
|
||||
const windowWidth = useWindowDimensions().width;
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
return StyleSheet.create({
|
||||
menu: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
menuContent: { flexDirection: 'column', width: '100%' },
|
||||
menuItem: {
|
||||
alignItems: 'flex-start',
|
||||
padding: theme.marginMedium,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
minWidth: Math.min(350, windowWidth),
|
||||
},
|
||||
menuItemContent: {
|
||||
flexDirection: 'row',
|
||||
gap: theme.marginSmall,
|
||||
alignItems: 'center',
|
||||
},
|
||||
menuItemText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
menuItemTextDisabled: {
|
||||
opacity: theme.disabledOpacity,
|
||||
},
|
||||
menuItemTextDestructive: {
|
||||
color: theme.destructiveColor,
|
||||
},
|
||||
divider: {
|
||||
height: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: theme.dividerColor,
|
||||
marginVertical: theme.marginSmall,
|
||||
marginHorizontal: theme.margin,
|
||||
},
|
||||
});
|
||||
}, [themeId, windowWidth]);
|
||||
};
|
||||
|
||||
const MenuComponent: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId);
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const menuOptionComponents: React.ReactNode[] = [];
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
let keyCounter = 0;
|
||||
for (const option of props.options) {
|
||||
if (option.isDivider === true) {
|
||||
menuOptionComponents.push(
|
||||
<View key={`menuOption_divider_${keyCounter++}`} style={styles.divider} />,
|
||||
);
|
||||
} else {
|
||||
const key = `menuOption_${option.key ?? keyCounter++}`;
|
||||
const textStyles: TextStyle[] = [styles.menuItemText];
|
||||
if (option.disabled) {
|
||||
textStyles.push(styles.menuItemTextDisabled);
|
||||
}
|
||||
if (option.style === MenuOptionStyle.Destructive) {
|
||||
textStyles.push(styles.menuItemTextDestructive);
|
||||
}
|
||||
|
||||
menuOptionComponents.push(
|
||||
<TouchableRipple
|
||||
borderless={true}
|
||||
role='button'
|
||||
style={styles.menuItem}
|
||||
onPress={() => {
|
||||
option.onPress();
|
||||
setOpen(false);
|
||||
}}
|
||||
key={key}
|
||||
disabled={!!option.disabled}
|
||||
>
|
||||
<View style={styles.menuItemContent}>
|
||||
{option.icon && <Icon name={option.icon} style={textStyles} accessibilityLabel={null} />}
|
||||
<Text
|
||||
style={textStyles}
|
||||
disabled={option.disabled}
|
||||
>{option.title}</Text>
|
||||
</View>
|
||||
</TouchableRipple>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const onMenuClosed = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, []);
|
||||
@@ -138,19 +30,14 @@ const MenuComponent: React.FC<Props> = props => {
|
||||
>
|
||||
{props.children}
|
||||
</TouchableRipple>
|
||||
<BottomDrawer
|
||||
<BottomDrawerMenu
|
||||
visible={open}
|
||||
onDismiss={onMenuClosed}
|
||||
draggable={true}
|
||||
style={styles.menu}
|
||||
>
|
||||
<View
|
||||
style={styles.menuContent}
|
||||
testID={`menu-content-${open ? 'open' : 'closed'}`}
|
||||
>
|
||||
{menuOptionComponents}
|
||||
</View>
|
||||
</BottomDrawer>
|
||||
// To match the side of the screen with the action button:
|
||||
alignment={MenuAlignment.Right}
|
||||
themeId={props.themeId}
|
||||
options={props.options}
|
||||
/>
|
||||
</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -24,8 +24,9 @@ import WarningBanner from './WarningBanner';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Icon from '../Icon';
|
||||
import Menu, { MenuOptionType } from './Menu';
|
||||
export { MenuOptionType };
|
||||
import Menu from './Menu';
|
||||
import { MenuOption, MenuOptionStyle } from '../BottomDrawerMenu';
|
||||
export { MenuOption, MenuOptionStyle };
|
||||
|
||||
// Rather than applying a padding to the whole bar, it is applied to each
|
||||
// individual component (button, picker, etc.) so that the touchable areas
|
||||
@@ -58,7 +59,7 @@ interface ScreenHeaderProps {
|
||||
showUndoButton: boolean;
|
||||
undoButtonDisabled?: boolean;
|
||||
showRedoButton: boolean;
|
||||
menuOptions: MenuOptionType[];
|
||||
menuOptions: MenuOption[];
|
||||
title?: string|null;
|
||||
folders: FolderEntity[];
|
||||
folderPickerOptions?: FolderPickerOptions;
|
||||
@@ -559,7 +560,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
);
|
||||
}
|
||||
|
||||
const menuOptions: MenuOptionType[] = [...this.props.menuOptions];
|
||||
const menuOptions: MenuOption[] = [...this.props.menuOptions];
|
||||
|
||||
const selectedFolder = this.props.notesParentType === 'Folder' ? Folder.byId(this.props.folders, this.props.selectedFolderId) : null;
|
||||
const selectedFolderInTrash = itemIsInTrash(selectedFolder);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { themeStyle } from '../global-style';
|
||||
import Logger, { LogEntry } from '@joplin/utils/Logger';
|
||||
import { BaseScreenComponent } from '../base-screen';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { MenuOptionType } from '../ScreenHeader';
|
||||
import { MenuOption } from '../ScreenHeader';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { writeTextToCacheFile } from '../../utils/ShareUtils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
@@ -30,7 +30,7 @@ interface State {
|
||||
}
|
||||
|
||||
class LogScreenComponent extends BaseScreenComponent<Props, State> {
|
||||
private readonly menuOptions_: MenuOptionType[];
|
||||
private readonly menuOptions_: MenuOption[];
|
||||
private styles_: Record<number, ReturnType<typeof StyleSheet.create>>;
|
||||
private readonly logListRef_ = React.createRef<FlatList>();
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import NavService, { OnNavigateCallback as OnNavigateCallback } from '@joplin/li
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { fileExtension, safeFileExtension } from '@joplin/lib/path-utils';
|
||||
import * as mimeUtils from '@joplin/lib/mime-utils';
|
||||
import ScreenHeader, { FolderPickerOptions, MenuOptionType, ViewToggleButtonMode } from '../../ScreenHeader';
|
||||
import ScreenHeader, { FolderPickerOptions, MenuOption, ViewToggleButtonMode } from '../../ScreenHeader';
|
||||
import NoteTagsDialog from '../NoteTagsDialog';
|
||||
import time from '@joplin/lib/time';
|
||||
import Checkbox from '../../Checkbox';
|
||||
@@ -84,7 +84,7 @@ import { Second } from '@joplin/utils/time';
|
||||
import TextWrapCalculator from '../Notes/TextWrapCalculator';
|
||||
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
|
||||
import { ALL_NOTES_FILTER_ID } from '@joplin/lib/reserved-ids';
|
||||
import { MenuOptionButton, MenuOptionStyle } from '../../ScreenHeader/Menu';
|
||||
import { MenuOptionStyle, MenuOptionButton } from '../../BottomDrawerMenu';
|
||||
|
||||
const emptyArray: never[] = [];
|
||||
|
||||
@@ -184,7 +184,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
private noteTagDialog_closeRequested: ()=> void;
|
||||
private refreshResource: (resource: ResourceEntity, noteBody?: string)=> Promise<void>;
|
||||
private selection: SelectionRange;
|
||||
private menuOptionsCache_: Record<string, MenuOptionType[]>;
|
||||
private menuOptionsCache_: Record<string, MenuOption[]>;
|
||||
private focusUpdateIID_: ReturnType<typeof setTimeout> | null;
|
||||
private folderPickerOptions_: FolderPickerOptions;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- dialogbox is the react-native-dialogbox ref; the library ships no types
|
||||
@@ -1292,7 +1292,7 @@ class NoteScreenComponent extends BaseScreenComponent<ComponentProps, State> imp
|
||||
|
||||
if (this.menuOptionsCache_[cacheKey]) return this.menuOptionsCache_[cacheKey];
|
||||
|
||||
const output: MenuOptionType[] = [];
|
||||
const output: MenuOption[] = [];
|
||||
|
||||
// The file attachment modules only work in Android >= 5 (Version 21)
|
||||
// https://github.com/react-community/react-native-image-picker/issues/606
|
||||
|
||||
@@ -7,7 +7,7 @@ import Revision from '@joplin/lib/models/Revision';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { IconButton, Text } from 'react-native-paper';
|
||||
import Dropdown from '../Dropdown';
|
||||
import ScreenHeader, { MenuOptionType } from '../ScreenHeader';
|
||||
import ScreenHeader, { MenuOption } from '../ScreenHeader';
|
||||
import { formatMsToLocal } from '@joplin/utils/time';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { PrimaryButton } from '../buttons';
|
||||
@@ -24,7 +24,7 @@ import { DialogContext } from '../DialogManager';
|
||||
import useDeleteHistoryClick from '@joplin/lib/components/shared/NoteRevisionViewer/useDeleteHistoryClick';
|
||||
import { OnScrollCallback } from '../NoteBodyViewer/types';
|
||||
import TextWrapCalculator from './Notes/TextWrapCalculator';
|
||||
import { MenuOptionStyle } from '../ScreenHeader/Menu';
|
||||
import { MenuOptionStyle } from '../BottomDrawerMenu';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -194,7 +194,7 @@ const NoteRevisionViewer: React.FC<Props> = props => {
|
||||
|
||||
const disableDeleteHistory = deleting || !hasRevisions;
|
||||
const menuOptions = useMemo(() => {
|
||||
const output: MenuOptionType[] = [{
|
||||
const output: MenuOption[] = [{
|
||||
title: _('Delete history'),
|
||||
icon: 'material delete-outline',
|
||||
style: MenuOptionStyle.Destructive,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo, useEffect, useCallback, useContext } from 'react';
|
||||
import { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Image, ImageStyle, Platform } from 'react-native';
|
||||
import { useMemo, useEffect, useCallback, useContext, useState } from 'react';
|
||||
import { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Image, ImageStyle } from 'react-native';
|
||||
import { Dispatch } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import Icon from './Icon';
|
||||
@@ -20,7 +20,6 @@ import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
||||
import emptyTrash from '@joplin/lib/services/trash/emptyTrash';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { DialogContext } from './DialogManager';
|
||||
import { PromptButtonSpec } from './DialogManager/types';
|
||||
import { TextStyle, ViewStyle } from 'react-native';
|
||||
import { StateDecryptionWorker, StateResourceFetcher } from '@joplin/lib/reducer';
|
||||
import useOnLongPressProps from '../utils/hooks/useOnLongPressProps';
|
||||
@@ -28,6 +27,8 @@ import { TouchableRipple } from 'react-native-paper';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import getConflictFolderId from '@joplin/lib/models/utils/getConflictFolderId';
|
||||
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
||||
import BottomDrawerMenu, { MenuOption, MenuOptionStyle } from './BottomDrawerMenu';
|
||||
import { MenuAlignment } from './BottomDrawer';
|
||||
|
||||
interface Props {
|
||||
syncStarted: boolean;
|
||||
@@ -343,23 +344,22 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const [folderMenuVisible, setFolderMenuVisible] = useState(false);
|
||||
const [folderMenuTitle, setFolderMenuTitle] = useState('');
|
||||
const [folderMenuOptions, setFolderMenuOptions] = useState<MenuOption[]>([]);
|
||||
const folder_longPress = async (folderOrAll: FolderEntity | string) => {
|
||||
if (folderOrAll === 'all') return;
|
||||
|
||||
const folder = folderOrAll as FolderEntity;
|
||||
|
||||
const menuItems: PromptButtonSpec[] = [];
|
||||
menuItems.push({
|
||||
text: _('Cancel'),
|
||||
onPress: () => {},
|
||||
style: 'cancel',
|
||||
});
|
||||
const menuItems: MenuOption[] = [];
|
||||
|
||||
if (folder && folder.id === getConflictFolderId()) return;
|
||||
|
||||
if (folder && folder.id === getTrashFolderId()) {
|
||||
menuItems.push({
|
||||
text: _('Empty trash'),
|
||||
title: _('Empty trash'),
|
||||
icon: 'material delete-outline',
|
||||
onPress: async () => {
|
||||
dialogs.prompt('', _('This will permanently delete all items in the trash. Continue?'), [
|
||||
{
|
||||
@@ -375,16 +375,16 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
},
|
||||
]);
|
||||
},
|
||||
style: 'destructive',
|
||||
style: MenuOptionStyle.Destructive,
|
||||
});
|
||||
|
||||
} else if (folder && !!folder.deleted_time) {
|
||||
menuItems.push({
|
||||
text: _('Restore'),
|
||||
title: _('Restore'),
|
||||
icon: 'material delete-off-outline',
|
||||
onPress: async () => {
|
||||
await restoreItems(ModelType.Folder, [folder.id]);
|
||||
},
|
||||
style: 'destructive',
|
||||
});
|
||||
|
||||
// Alert.alert(
|
||||
@@ -434,13 +434,15 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
return folderDeletion(_('Move notebook "%s" to the trash?\n\nAll notes and sub-notebooks within this notebook will also be moved to the trash.', substrWithEllipsis(folder.title, 0, 32)));
|
||||
};
|
||||
|
||||
const deleteButton: PromptButtonSpec = {
|
||||
text: _('Delete'),
|
||||
const deleteButton: MenuOption = {
|
||||
title: _('Delete'),
|
||||
onPress: generateFolderDeletion,
|
||||
style: 'destructive',
|
||||
icon: 'material delete-outline',
|
||||
style: MenuOptionStyle.Destructive,
|
||||
};
|
||||
const editButton: PromptButtonSpec = {
|
||||
text: _('Edit'),
|
||||
const editButton: MenuOption = {
|
||||
title: _('Edit'),
|
||||
icon: 'material file-edit-outline',
|
||||
onPress: () => {
|
||||
props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
@@ -452,20 +454,12 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
},
|
||||
};
|
||||
|
||||
// On iOS, "cancel" is always shown at the bottom. Push the edit button first
|
||||
// so that "delete" is still shown in the middle.
|
||||
if (Platform.OS === 'ios') {
|
||||
menuItems.push(editButton, deleteButton);
|
||||
} else {
|
||||
menuItems.push(deleteButton, editButton);
|
||||
}
|
||||
menuItems.push(editButton, deleteButton);
|
||||
}
|
||||
|
||||
dialogs.prompt(
|
||||
'',
|
||||
_('Notebook: %s', folder.title),
|
||||
menuItems,
|
||||
);
|
||||
setFolderMenuOptions(menuItems);
|
||||
setFolderMenuTitle(_('Notebook: %s', folder.title));
|
||||
setFolderMenuVisible(true);
|
||||
};
|
||||
|
||||
const folder_togglePress = (folder: FolderEntity) => {
|
||||
@@ -740,6 +734,14 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
</ScrollView>
|
||||
{renderBottomPanel()}
|
||||
</View>
|
||||
<BottomDrawerMenu
|
||||
visible={folderMenuVisible}
|
||||
onDismiss={() => setFolderMenuVisible(false)}
|
||||
themeId={props.themeId}
|
||||
alignment={MenuAlignment.Center}
|
||||
title={folderMenuTitle}
|
||||
options={folderMenuOptions}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user