xref: /MusicFree/src/components/base/appBar.tsx (revision e0caf3427c927f4cf2efda9969f9a6f4fb0ef565)
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.appBarText;
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, index) => (
137                    <IconButton
138                        key={index}
139                        name={action.icon}
140                        sizeType="normal"
141                        color={contentColor}
142                        style={[globalStyle.notShrink, styles.rightButton]}
143                        onPress={action.onPress}
144                    />
145                ))}
146                {actionComponent ?? null}
147                {menu?.length ? (
148                    <IconButton
149                        name="dots-vertical"
150                        sizeType="normal"
151                        onLayout={e => {
152                            menuIconLayoutRef.current = e.nativeEvent.layout;
153                        }}
154                        color={contentColor}
155                        style={[globalStyle.notShrink, styles.rightButton]}
156                        onPress={() => {
157                            setShowMenu(true);
158                        }}
159                    />
160                ) : null}
161            </View>
162            <Portal>
163                {showMenu ? (
164                    <TouchableWithoutFeedback
165                        onPress={() => {
166                            setShowMenu(false);
167                        }}>
168                        <View style={styles.blocker} />
169                    </TouchableWithoutFeedback>
170                ) : null}
171                <>
172                    <Animated.View
173                        pointerEvents={showMenu ? 'auto' : 'none'}
174                        style={[
175                            {
176                                borderBottomColor: colors.background,
177                                left:
178                                    (menuIconLayoutRef.current?.x ?? 0) +
179                                    (menuIconLayoutRef.current?.width ?? 0) /
180                                        2 -
181                                    rpx(10),
182                                top:
183                                    (menuIconLayoutRef.current?.y ?? 0) +
184                                    (menuIconLayoutRef.current?.height ?? 0) +
185                                    (menuWithStatusBar
186                                        ? OriginalStatusBar.currentHeight ?? 0
187                                        : 0),
188                            },
189                            transformStyle,
190                            styles.bubbleCorner,
191                        ]}
192                    />
193                    <Animated.View
194                        pointerEvents={showMenu ? 'auto' : 'none'}
195                        style={[
196                            {
197                                backgroundColor: colors.background,
198                                right: rpx(24),
199                                top:
200                                    (menuIconLayoutRef.current?.y ?? 0) +
201                                    (menuIconLayoutRef.current?.height ?? 0) +
202                                    rpx(20) +
203                                    (menuWithStatusBar
204                                        ? OriginalStatusBar.currentHeight ?? 0
205                                        : 0),
206                                shadowColor: colors.shadow,
207                            },
208                            transformStyle,
209                            styles.menu,
210                        ]}>
211                        {menu.map(it =>
212                            it.show !== false ? (
213                                <ListItem
214                                    key={it.title}
215                                    withHorizonalPadding
216                                    heightType="small"
217                                    onPress={() => {
218                                        it.onPress?.();
219                                        setShowMenu(false);
220                                    }}>
221                                    <ListItem.ListItemIcon icon={it.icon} />
222                                    <ListItem.Content title={it.title} />
223                                </ListItem>
224                            ) : null,
225                        )}
226                    </Animated.View>
227                </>
228            </Portal>
229        </>
230    );
231}
232
233const styles = StyleSheet.create({
234    container: {
235        width: '100%',
236        zIndex: 10000,
237        height: rpx(88),
238        flexDirection: 'row',
239        paddingHorizontal: rpx(24),
240    },
241    content: {
242        flexDirection: 'row',
243        flexBasis: 0,
244        alignItems: 'center',
245        paddingHorizontal: rpx(24),
246    },
247    rightButton: {
248        marginLeft: rpx(28),
249    },
250    blocker: {
251        position: 'absolute',
252        bottom: 0,
253        left: 0,
254        width: '100%',
255        height: '100%',
256        zIndex: 10010,
257    },
258    bubbleCorner: {
259        position: 'absolute',
260        borderColor: 'transparent',
261        borderWidth: rpx(10),
262        zIndex: 10012,
263        transformOrigin: 'right top',
264        opacity: 0,
265    },
266    menu: {
267        width: rpx(340),
268        maxHeight: rpx(600),
269        borderRadius: rpx(8),
270        zIndex: 10011,
271        position: 'absolute',
272        opacity: 0,
273        shadowOffset: {
274            width: 0,
275            height: 2,
276        },
277        shadowOpacity: 0.23,
278        shadowRadius: 2.62,
279        elevation: 4,
280    },
281});
282