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