xref: /MusicFree/src/components/base/appBar.tsx (revision 410a159129b1f6a7a1f44fde7bfad9a46f91e161)
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    titleTextOpacity?: number;
30    withStatusBar?: boolean;
31    color?: string;
32    actions?: Array<{
33        icon: string;
34        onPress?: () => void;
35    }>;
36    menu?: Array<{
37        icon: string;
38        title: string;
39        show?: boolean;
40        onPress?: () => void;
41    }>;
42    menuWithStatusBar?: boolean;
43    children?: string | ReactNode;
44    containerStyle?: StyleProp<ViewStyle>;
45    contentStyle?: StyleProp<ViewStyle>;
46    actionComponent?: ReactNode;
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        titleTextOpacity = 1,
60        withStatusBar,
61        color: _color,
62        actions = [],
63        menu = [],
64        menuWithStatusBar = true,
65        containerStyle,
66        contentStyle,
67        children,
68        actionComponent,
69    } = props;
70
71    const colors = useColors();
72    const navigation = useNavigation();
73
74    const bgColor = color(colors.appBar ?? colors.primary).toString();
75    const contentColor = _color ?? colors.appBarText;
76
77    const [showMenu, setShowMenu] = useState(false);
78    const menuIconLayoutRef = useRef<LayoutRectangle>();
79    const scaleRate = useSharedValue(0);
80
81    useEffect(() => {
82        if (showMenu) {
83            scaleRate.value = withTiming(1, timingConfig);
84        } else {
85            scaleRate.value = withTiming(0, timingConfig);
86        }
87    }, [showMenu]);
88
89    const transformStyle = useAnimatedStyle(() => {
90        return {
91            opacity: scaleRate.value,
92        };
93    });
94
95    return (
96        <>
97            {withStatusBar ? <StatusBar backgroundColor={bgColor} /> : null}
98            <View
99                style={[
100                    styles.container,
101                    containerStyle,
102                    {backgroundColor: bgColor},
103                ]}>
104                <IconButton
105                    name="arrow-left"
106                    sizeType="normal"
107                    color={contentColor}
108                    style={globalStyle.notShrink}
109                    onPress={() => {
110                        navigation.goBack();
111                    }}
112                />
113                <View style={[globalStyle.grow, styles.content, contentStyle]}>
114                    {typeof children === 'string' ? (
115                        <ThemeText
116                            fontSize="title"
117                            fontWeight="bold"
118                            numberOfLines={1}
119                            color={
120                                titleTextOpacity !== 1
121                                    ? color(contentColor)
122                                          .alpha(titleTextOpacity)
123                                          .toString()
124                                    : contentColor
125                            }>
126                            {children}
127                        </ThemeText>
128                    ) : (
129                        children
130                    )}
131                </View>
132                {actions.map((action, index) => (
133                    <IconButton
134                        key={index}
135                        name={action.icon}
136                        sizeType="normal"
137                        color={contentColor}
138                        style={[globalStyle.notShrink, styles.rightButton]}
139                        onPress={action.onPress}
140                    />
141                ))}
142                {actionComponent ?? null}
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.background,
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                                    key={it.title}
211                                    withHorizonalPadding
212                                    heightType="small"
213                                    onPress={() => {
214                                        it.onPress?.();
215                                        setShowMenu(false);
216                                    }}>
217                                    <ListItem.ListItemIcon icon={it.icon} />
218                                    <ListItem.Content title={it.title} />
219                                </ListItem>
220                            ) : null,
221                        )}
222                    </Animated.View>
223                </>
224            </Portal>
225        </>
226    );
227}
228
229const styles = StyleSheet.create({
230    container: {
231        width: '100%',
232        zIndex: 10000,
233        height: rpx(88),
234        flexDirection: 'row',
235        paddingHorizontal: rpx(24),
236    },
237    content: {
238        flexDirection: 'row',
239        flexBasis: 0,
240        alignItems: 'center',
241        paddingHorizontal: rpx(24),
242    },
243    rightButton: {
244        marginLeft: rpx(28),
245    },
246    blocker: {
247        position: 'absolute',
248        bottom: 0,
249        left: 0,
250        width: '100%',
251        height: '100%',
252        zIndex: 10010,
253    },
254    bubbleCorner: {
255        position: 'absolute',
256        borderColor: 'transparent',
257        borderWidth: rpx(10),
258        zIndex: 10012,
259        transformOrigin: 'right top',
260        opacity: 0,
261    },
262    menu: {
263        width: rpx(340),
264        maxHeight: rpx(600),
265        borderRadius: rpx(8),
266        zIndex: 10011,
267        position: 'absolute',
268        opacity: 0,
269        shadowOffset: {
270            width: 0,
271            height: 2,
272        },
273        shadowOpacity: 0.23,
274        shadowRadius: 2.62,
275        elevation: 4,
276    },
277});
278