feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,164 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useCallback, useMemo, useRef, RefObject, useLayoutEffect } from 'react';
|
||||
import { FAB } from 'react-native-paper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { Dispatch } from 'redux';
|
||||
import { AccessibilityActionEvent, AccessibilityActionInfo, StyleSheet, useWindowDimensions, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { MenuAlignment, MenuType } from '../BottomDrawer';
|
||||
import { Ionicons as Icon } from '@react-native-vector-icons/ionicons';
|
||||
import BottomDrawerMenu, { MenuOption } from '../BottomDrawerMenu';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { themeStyle } from '../global-style';
|
||||
|
||||
type OnButtonPress = ()=> void;
|
||||
interface ButtonSpec {
|
||||
icon: string;
|
||||
label: string;
|
||||
color?: string;
|
||||
onPress?: OnButtonPress;
|
||||
}
|
||||
|
||||
interface ActionButtonProps {
|
||||
themeId: number;
|
||||
|
||||
// If not given, an "add" button will be used.
|
||||
mainButton: ButtonSpec;
|
||||
dispatch: Dispatch;
|
||||
|
||||
menuContent?: MenuOption[];
|
||||
onMenuShow?: ()=> void;
|
||||
|
||||
accessibilityActions?: readonly AccessibilityActionInfo[];
|
||||
// Can return a Promise to simplify unit testing
|
||||
onAccessibilityAction?: (event: AccessibilityActionEvent)=> void|Promise<void>;
|
||||
accessibilityHint?: string;
|
||||
}
|
||||
|
||||
// Returns a render function compatible with React Native Paper.
|
||||
const getIconRenderFunction = (iconName: string) => {
|
||||
type NameProp = React.ComponentProps<typeof Icon>['name'];
|
||||
return (props: Omit<React.ComponentProps<typeof Icon>, 'name'>) => <Icon name={iconName as NameProp} {...props} />;
|
||||
};
|
||||
|
||||
const useIcon = (iconName: string) => {
|
||||
return useMemo(() => {
|
||||
return getIconRenderFunction(iconName);
|
||||
}, [iconName]);
|
||||
};
|
||||
|
||||
const useMenuMarginBottom = (buttonContainerRef: RefObject<View>, themeId: number) => {
|
||||
const { height: windowHeight } = useWindowDimensions();
|
||||
const [menuMarginBottom, setMenuMarginBottom] = useState(0);
|
||||
|
||||
const recomputeMargin = useCallback(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
buttonContainerRef.current?.measure((_x, _y, _width, _height, _pageX, pageY) => {
|
||||
const extraMargin = theme.marginBottom;
|
||||
setMenuMarginBottom(windowHeight - pageY + extraMargin);
|
||||
});
|
||||
}, [windowHeight, buttonContainerRef, themeId]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
recomputeMargin();
|
||||
}, [recomputeMargin]);
|
||||
|
||||
const recomputeMarginRef = useRef(recomputeMargin);
|
||||
recomputeMarginRef.current = recomputeMargin;
|
||||
|
||||
const onMenuContainerLayout = useCallback(() => {
|
||||
recomputeMarginRef.current();
|
||||
}, []);
|
||||
|
||||
return { onMenuContainerLayout, menuMarginBottom };
|
||||
};
|
||||
|
||||
|
||||
interface StylesProps {
|
||||
menuMarginBottom: number;
|
||||
}
|
||||
|
||||
const useStyles = ({ menuMarginBottom }: StylesProps) => {
|
||||
return useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
menu: {
|
||||
marginBottom: menuMarginBottom,
|
||||
// Always float right:
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
buttonContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
},
|
||||
button: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
});
|
||||
}, [menuMarginBottom]);
|
||||
};
|
||||
|
||||
const FloatingActionButton = (props: ActionButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const onMenuToggled = useCallback(() => {
|
||||
const newOpen = !open;
|
||||
if (newOpen) {
|
||||
props.dispatch({
|
||||
type: 'SIDE_MENU_CLOSE',
|
||||
});
|
||||
}
|
||||
setOpen(newOpen);
|
||||
}, [setOpen, open, props.dispatch]);
|
||||
|
||||
const onDismiss = useCallback(() => {
|
||||
if (open) onMenuToggled();
|
||||
}, [open, onMenuToggled]);
|
||||
|
||||
const mainButtonRef = useRef<View>(null);
|
||||
|
||||
const closedIcon = useIcon(props.mainButton?.icon ?? 'add');
|
||||
const openIcon = useIcon('close');
|
||||
|
||||
const label = props.mainButton?.label ?? _('Add new');
|
||||
|
||||
const buttonContainerRef = useRef<View|null>(null);
|
||||
|
||||
const { menuMarginBottom, onMenuContainerLayout } = useMenuMarginBottom(buttonContainerRef, props.themeId);
|
||||
const styles = useStyles({ menuMarginBottom });
|
||||
|
||||
const menuButton = <FAB
|
||||
ref={mainButtonRef}
|
||||
icon={open ? openIcon : closedIcon}
|
||||
accessibilityLabel={label}
|
||||
onPress={props.mainButton?.onPress ?? onMenuToggled}
|
||||
style={styles.button}
|
||||
accessibilityActions={props.accessibilityActions}
|
||||
onAccessibilityAction={props.onAccessibilityAction}
|
||||
/>;
|
||||
|
||||
return <>
|
||||
<View
|
||||
style={styles.buttonContainer}
|
||||
ref={buttonContainerRef}
|
||||
onLayout={onMenuContainerLayout}
|
||||
>
|
||||
{menuButton}
|
||||
</View>
|
||||
{ props.menuContent && <BottomDrawerMenu
|
||||
visible={open}
|
||||
onDismiss={onDismiss}
|
||||
alignment={MenuAlignment.Right}
|
||||
style={styles.menu}
|
||||
menuType={MenuType.Floating}
|
||||
themeId={props.themeId}
|
||||
options={props.menuContent}
|
||||
autoScrollToEnd
|
||||
/> }
|
||||
</>;
|
||||
};
|
||||
|
||||
const ConnectedComponent: React.FC<Omit<ActionButtonProps, 'themeId'|'dispatch'>> = (
|
||||
connect((state: AppState) => ({ themeId: state.settings.theme }))(FloatingActionButton)
|
||||
);
|
||||
export default ConnectedComponent;
|
||||
在新工单中引用
屏蔽一个用户