xref: /MusicFree/src/components/base/appBar.tsx (revision ab55f125072c3b77549324c638fbca1fe4561337)
1import React, {ReactNode, useEffect, useRef, useState} from 'react';
2import {
3    LayoutRectangle,
4    StyleSheet,
5    TouchableWithoutFeedback,
6    View,
7    StatusBar as OriginalStatusBar,
8    StyleProp,
9    ViewStyle,
10} from 'react-native';
11import rpx from '@/utils/rpx';
12import useColors from '@/hooks/useColors';
13import StatusBar from './statusBar';
14import color from 'color';
15import IconButton from './iconButton';
16import globalStyle from '@/constants/globalStyle';
17import ThemeText from './themeText';
18import {useNavigation} from '@react-navigation/native';
19import Animated, {
20    Easing,
21    useAnimatedStyle,
22    useSharedValue,
23    withTiming,
24} from 'react-native-reanimated';
25import Portal from './portal';
26import ListItem from './listItem';
27
28interface IAppBarProps {
29    backgroundOpacity?: number;
30    titleTextOpacity?: number;
31    withStatusBar?: boolean;
32    color?: string;
33    actions?: Array<{
34        icon: string;
35        onPress?: () => void;
36    }>;
37    menu?: Array<{
38        icon: string;
39        title: string;
40        show?: boolean;
41        onPress?: () => void;
42    }>;
43    menuWithStatusBar?: boolean;
44    children?: string | ReactNode;
45    containerStyle?: StyleProp<ViewStyle>;
46    contentStyle?: StyleProp<ViewStyle>;
47    actionComponent?: ReactNode;
48}
49
50const ANIMATION_EASING: Animated.EasingFunction = Easing.out(Easing.exp);
51const ANIMATION_DURATION = 500;
52
53const timingConfig = {
54    duration: ANIMATION_DURATION,
55    easing: ANIMATION_EASING,
56};
57
58export default function AppBar(props: IAppBarProps) {
59    const {
60        backgroundOpacity = 1,
61        titleTextOpacity = 1,
62        withStatusBar,
63        color: _color,
64        actions = [],
65        menu = [],
66        menuWithStatusBar = true,
67        containerStyle,
68        contentStyle,
69        children,
70        actionComponent,
71    } = props;
72
73    const colors = useColors();
74    const navigation = useNavigation();
75
76    const bgColor = color(colors.appBar ?? colors.primary)
77        .alpha(backgroundOpacity)
78        .toString();
79    const contentColor = _color ?? colors.headerText;
80
81    const [showMenu, setShowMenu] = useState(false);
82    const menuIconLayoutRef = useRef<LayoutRectangle>();
83    const scaleRate = useSharedValue(0);
84
85    useEffect(() => {
86        if (showMenu) {
87            scaleRate.value = withTiming(1, timingConfig);
88        } else {
89            scaleRate.value = withTiming(0, timingConfig);
90        }
91    }, [showMenu]);
92
93    const transformStyle = useAnimatedStyle(() => {
94        return {
95            opacity: scaleRate.value,
96        };
97    });
98
99    return (
100        <>
101            {withStatusBar ? <StatusBar backgroundColor={bgColor} /> : null}
102            <View
103                style={[
104                    styles.container,
105                    containerStyle,
106                    {backgroundColor: bgColor},
107                ]}>
108                <IconButton
109                    name="arrow-left"
110                    sizeType="normal"
111                    color={contentColor}
112                    style={globalStyle.notShrink}
113                    onPress={() => {
114                        navigation.goBack();
115                    }}
116                />
117                <View style={[globalStyle.grow, styles.content, contentStyle]}>
118                    {typeof children === 'string' ? (
119                        <ThemeText
120                            fontSize="title"
121                            fontWeight="bold"
122                            numberOfLines={1}
123                            color={
124                                titleTextOpacity !== 1
125                                    ? color(contentColor)
126                                          .alpha(titleTextOpacity)
127                                          .toString()
128                                    : contentColor
129                            }>
130                            {children}
131                        </ThemeText>
132                    ) : (
133                        children
134                    )}
135                </View>
136                {actions.map(action => (
137                    <IconButton
138                        name={action.icon}
139                        sizeType="normal"
140                        color={contentColor}
141                        style={[globalStyle.notShrink, styles.rightButton]}
142                        onPress={action.onPress}
143                    />
144                ))}
145                {actionComponent ?? null}
146                {menu?.length ? (
147                    <IconButton
148                        name="dots-vertical"
149                        sizeType="normal"
150                        onLayout={e => {
151                            menuIconLayoutRef.current = e.nativeEvent.layout;
152                        }}
153                        color={contentColor}
154                        style={[globalStyle.notShrink, styles.rightButton]}
155                        onPress={() => {
156                            setShowMenu(true);
157                        }}
158                    />
159                ) : null}
160            </View>
161            <Portal>
162                {showMenu ? (
163                    <TouchableWithoutFeedback
164                        onPress={() => {
165                            setShowMenu(false);
166                        }}>
167                        <View style={styles.blocker} />
168                    </TouchableWithoutFeedback>
169                ) : null}
170                <>
171                    <Animated.View
172                        pointerEvents={showMenu ? 'auto' : 'none'}
173                        style={[
174                            {
175                                borderBottomColor: colors.background,
176                                left:
177                                    (menuIconLayoutRef.current?.x ?? 0) +
178                                    (menuIconLayoutRef.current?.width ?? 0) /
179                                        2 -
180                                    rpx(10),
181                                top:
182                                    (menuIconLayoutRef.current?.y ?? 0) +
183                                    (menuIconLayoutRef.current?.height ?? 0) +
184                                    (menuWithStatusBar
185                                        ? OriginalStatusBar.currentHeight ?? 0
186                                        : 0),
187                            },
188                            transformStyle,
189                            styles.bubbleCorner,
190                        ]}
191                    />
192                    <Animated.View
193                        pointerEvents={showMenu ? 'auto' : 'none'}
194                        style={[
195                            {
196                                backgroundColor: colors.background,
197                                right: rpx(24),
198                                top:
199                                    (menuIconLayoutRef.current?.y ?? 0) +
200                                    (menuIconLayoutRef.current?.height ?? 0) +
201                                    rpx(20) +
202                                    (menuWithStatusBar
203                                        ? OriginalStatusBar.currentHeight ?? 0
204                                        : 0),
205                                shadowColor: colors.shadow,
206                            },
207                            transformStyle,
208                            styles.menu,
209                        ]}>
210                        {menu.map(it =>
211                            it.show !== false ? (
212                                <ListItem
213                                    withHorizonalPadding
214                                    heightType="small"
215                                    onPress={() => {
216                                        it.onPress?.();
217                                        setShowMenu(false);
218                                    }}>
219                                    <ListItem.ListItemIcon icon={it.icon} />
220                                    <ListItem.Content title={it.title} />
221                                </ListItem>
222                            ) : null,
223                        )}
224                    </Animated.View>
225                </>
226            </Portal>
227        </>
228    );
229}
230
231const styles = StyleSheet.create({
232    container: {
233        width: '100%',
234        zIndex: 10000,
235        height: rpx(88),
236        flexDirection: 'row',
237        paddingHorizontal: rpx(24),
238    },
239    content: {
240        flexDirection: 'row',
241        flexBasis: 0,
242        alignItems: 'center',
243        paddingHorizontal: rpx(24),
244    },
245    rightButton: {
246        marginLeft: rpx(28),
247    },
248    blocker: {
249        position: 'absolute',
250        bottom: 0,
251        left: 0,
252        width: '100%',
253        height: '100%',
254        zIndex: 10010,
255    },
256    bubbleCorner: {
257        position: 'absolute',
258        borderColor: 'transparent',
259        borderWidth: rpx(10),
260        zIndex: 10012,
261        transformOrigin: 'right top',
262        opacity: 0,
263    },
264    menu: {
265        width: rpx(340),
266        maxHeight: rpx(600),
267        borderRadius: rpx(8),
268        zIndex: 10011,
269        position: 'absolute',
270        opacity: 0,
271        shadowOffset: {
272            width: 0,
273            height: 2,
274        },
275        shadowOpacity: 0.23,
276        shadowRadius: 2.62,
277        elevation: 4,
278    },
279});
280