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