xref: /MusicFree/src/components/base/appBar.tsx (revision 5589cdf32b2bb0f641e5ac7bf1f6152cd6b9b70e)
1import React, {ReactNode, useEffect, useState} from 'react';
2import {
3    LayoutRectangle,
4    StatusBar as OriginalStatusBar,
5    StyleProp,
6    StyleSheet,
7    TouchableWithoutFeedback,
8    View,
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';
27import {IIconName} from '@/components/base/icon.tsx';
28
29interface IAppBarProps {
30    titleTextOpacity?: number;
31    withStatusBar?: boolean;
32    color?: string;
33    actions?: Array<{
34        icon: IIconName;
35        onPress?: () => void;
36    }>;
37    menu?: Array<{
38        icon: IIconName;
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        titleTextOpacity = 1,
61        withStatusBar,
62        color: _color,
63        actions = [],
64        menu = [],
65        menuWithStatusBar = true,
66        containerStyle,
67        contentStyle,
68        children,
69        actionComponent,
70    } = props;
71
72    const colors = useColors();
73    const navigation = useNavigation();
74
75    const bgColor = color(colors.appBar ?? colors.primary).toString();
76    const contentColor = _color ?? colors.appBarText;
77
78    const [showMenu, setShowMenu] = useState(false);
79    const [menuIconLayout, setMenuIconLayout] =
80        useState<LayoutRectangle | null>(null);
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, index) => (
135                    <IconButton
136                        key={index}
137                        name={action.icon}
138                        sizeType="normal"
139                        color={contentColor}
140                        style={[globalStyle.notShrink, styles.rightButton]}
141                        onPress={action.onPress}
142                    />
143                ))}
144                {actionComponent ?? null}
145                {menu?.length ? (
146                    <IconButton
147                        name="ellipsis-vertical"
148                        sizeType="normal"
149                        onLayout={evt => {
150                            setMenuIconLayout(evt.nativeEvent.layout);
151                        }}
152                        color={contentColor}
153                        style={[globalStyle.notShrink, styles.rightButton]}
154                        onPress={() => {
155                            setShowMenu(true);
156                        }}
157                    />
158                ) : null}
159            </View>
160            <Portal>
161                {showMenu ? (
162                    <TouchableWithoutFeedback
163                        onPress={() => {
164                            setShowMenu(false);
165                        }}>
166                        <View style={styles.blocker} />
167                    </TouchableWithoutFeedback>
168                ) : null}
169                <>
170                    <Animated.View
171                        pointerEvents={showMenu ? 'auto' : 'none'}
172                        style={[
173                            {
174                                borderBottomColor: colors.background,
175                                left:
176                                    (menuIconLayout?.x ?? 0) +
177                                    (menuIconLayout?.width ?? 0) / 2 -
178                                    rpx(10),
179                                top:
180                                    (menuIconLayout?.y ?? 0) +
181                                    (menuIconLayout?.height ?? 0) +
182                                    (menuWithStatusBar
183                                        ? OriginalStatusBar.currentHeight ?? 0
184                                        : 0),
185                            },
186                            transformStyle,
187                            styles.bubbleCorner,
188                        ]}
189                    />
190                    <Animated.View
191                        pointerEvents={showMenu ? 'auto' : 'none'}
192                        style={[
193                            {
194                                backgroundColor: colors.background,
195                                right: rpx(24),
196                                top:
197                                    (menuIconLayout?.y ?? 0) +
198                                    (menuIconLayout?.height ?? 0) +
199                                    rpx(20) +
200                                    (menuWithStatusBar
201                                        ? OriginalStatusBar.currentHeight ?? 0
202                                        : 0),
203                                shadowColor: colors.shadow,
204                            },
205                            transformStyle,
206                            styles.menu,
207                        ]}>
208                        {menu.map(it =>
209                            it.show !== false ? (
210                                <ListItem
211                                    key={it.title}
212                                    withHorizontalPadding
213                                    heightType="small"
214                                    onPress={() => {
215                                        it.onPress?.();
216                                        setShowMenu(false);
217                                    }}>
218                                    <ListItem.ListItemIcon icon={it.icon} />
219                                    <ListItem.Content title={it.title} />
220                                </ListItem>
221                            ) : null,
222                        )}
223                    </Animated.View>
224                </>
225            </Portal>
226        </>
227    );
228}
229
230const styles = StyleSheet.create({
231    container: {
232        width: '100%',
233        zIndex: 10000,
234        height: rpx(88),
235        flexDirection: 'row',
236        alignItems: 'center',
237        paddingHorizontal: rpx(24),
238    },
239    content: {
240        flexDirection: 'row',
241        flexBasis: 0,
242        alignItems: 'center',
243        paddingHorizontal: rpx(24),
244    },
245    rightButton: {
246        marginLeft: rpx(28),
247    },
248    blocker: {
249        position: 'absolute',
250        bottom: 0,
251        left: 0,
252        width: '100%',
253        height: '100%',
254        zIndex: 10010,
255    },
256    bubbleCorner: {
257        position: 'absolute',
258        borderColor: 'transparent',
259        borderWidth: rpx(10),
260        zIndex: 10012,
261        transformOrigin: 'right top',
262        opacity: 0,
263    },
264    menu: {
265        width: rpx(340),
266        maxHeight: rpx(600),
267        borderRadius: rpx(8),
268        zIndex: 10011,
269        position: 'absolute',
270        opacity: 0,
271        shadowOffset: {
272            width: 0,
273            height: 2,
274        },
275        shadowOpacity: 0.23,
276        shadowRadius: 2.62,
277        elevation: 4,
278    },
279});
280