xref: /MusicFree/src/components/base/appBar.tsx (revision bcc589474c419ce80ca7acdb6859d807e589bd99)
1import React, {ReactNode, useEffect, 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 [menuIconLayout, setMenuIconLayout] =
79        useState<LayoutRectangle | null>(null);
80    const scaleRate = useSharedValue(0);
81
82    useEffect(() => {
83        if (showMenu) {
84            scaleRate.value = withTiming(1, timingConfig);
85        } else {
86            scaleRate.value = withTiming(0, timingConfig);
87        }
88    }, [showMenu]);
89
90    const transformStyle = useAnimatedStyle(() => {
91        return {
92            opacity: scaleRate.value,
93        };
94    });
95
96    return (
97        <>
98            {withStatusBar ? <StatusBar backgroundColor={bgColor} /> : null}
99            <View
100                style={[
101                    styles.container,
102                    containerStyle,
103                    {backgroundColor: bgColor},
104                ]}>
105                <IconButton
106                    name="arrow-left"
107                    sizeType="normal"
108                    color={contentColor}
109                    style={globalStyle.notShrink}
110                    onPress={() => {
111                        navigation.goBack();
112                    }}
113                />
114                <View style={[globalStyle.grow, styles.content, contentStyle]}>
115                    {typeof children === 'string' ? (
116                        <ThemeText
117                            fontSize="title"
118                            fontWeight="bold"
119                            numberOfLines={1}
120                            color={
121                                titleTextOpacity !== 1
122                                    ? color(contentColor)
123                                          .alpha(titleTextOpacity)
124                                          .toString()
125                                    : contentColor
126                            }>
127                            {children}
128                        </ThemeText>
129                    ) : (
130                        children
131                    )}
132                </View>
133                {actions.map((action, index) => (
134                    <IconButton
135                        key={index}
136                        name={action.icon}
137                        sizeType="normal"
138                        color={contentColor}
139                        style={[globalStyle.notShrink, styles.rightButton]}
140                        onPress={action.onPress}
141                    />
142                ))}
143                {actionComponent ?? null}
144                {menu?.length ? (
145                    <IconButton
146                        name="dots-vertical"
147                        sizeType="normal"
148                        onLayout={e => {
149                            setMenuIconLayout(e.nativeEvent.layout);
150                        }}
151                        color={contentColor}
152                        style={[globalStyle.notShrink, styles.rightButton]}
153                        onPress={() => {
154                            setShowMenu(true);
155                        }}
156                    />
157                ) : null}
158            </View>
159            <Portal>
160                {showMenu ? (
161                    <TouchableWithoutFeedback
162                        onPress={() => {
163                            setShowMenu(false);
164                        }}>
165                        <View style={styles.blocker} />
166                    </TouchableWithoutFeedback>
167                ) : null}
168                <>
169                    <Animated.View
170                        pointerEvents={showMenu ? 'auto' : 'none'}
171                        style={[
172                            {
173                                borderBottomColor: colors.background,
174                                left:
175                                    (menuIconLayout?.x ?? 0) +
176                                    (menuIconLayout?.width ?? 0) / 2 -
177                                    rpx(10),
178                                top:
179                                    (menuIconLayout?.y ?? 0) +
180                                    (menuIconLayout?.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                                    (menuIconLayout?.y ?? 0) +
197                                    (menuIconLayout?.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