xref: /MusicFree/src/pages/musicDetail/components/content/lyric/index.tsx (revision 410a159129b1f6a7a1f44fde7bfad9a46f91e161)
1import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
2import {LayoutRectangle, StyleSheet, Text, View} from 'react-native';
3import rpx from '@/utils/rpx';
4import useDelayFalsy from '@/hooks/useDelayFalsy';
5import {
6    FlatList,
7    Gesture,
8    GestureDetector,
9    TapGestureHandler,
10} from 'react-native-gesture-handler';
11import {fontSizeConst} from '@/constants/uiConst';
12import {IconButtonWithGesture} from '@/components/base/iconButton';
13import Loading from '@/components/base/loading';
14import globalStyle from '@/constants/globalStyle';
15import {showPanel} from '@/components/panels/usePanel';
16import LyricManager from '@/core/lyricManager';
17import TrackPlayer from '@/core/trackPlayer';
18import {musicIsPaused} from '@/utils/trackUtils';
19import delay from '@/utils/delay';
20import DraggingTime from './draggingTime';
21import LyricItemComponent from './lyricItem';
22import PersistStatus from '@/core/persistStatus';
23import LyricOperations from './lyricOperations';
24import MediaExtra from '@/core/mediaExtra';
25import {IParsedLrcItem} from '@/utils/lrcParser';
26
27const ITEM_HEIGHT = rpx(92);
28
29interface IItemHeights {
30    blankHeight?: number;
31    [k: number]: number;
32}
33
34interface IProps {
35    onTurnPageClick?: () => void;
36}
37
38const fontSizeMap = {
39    0: rpx(24),
40    1: rpx(30),
41    2: rpx(36),
42    3: rpx(42),
43} as Record<number, number>;
44
45export default function Lyric(props: IProps) {
46    const {onTurnPageClick} = props;
47
48    const {loading, meta, lyrics, hasTranslation} =
49        LyricManager.useLyricState();
50    const currentLrcItem = LyricManager.useCurrentLyric();
51    const showTranslation = PersistStatus.useValue(
52        'lyric.showTranslation',
53        false,
54    );
55    const fontSizeKey = PersistStatus.useValue('lyric.detailFontSize', 1);
56    const fontSizeStyle = useMemo(
57        () => ({
58            fontSize: fontSizeMap[fontSizeKey!],
59        }),
60        [fontSizeKey],
61    );
62
63    const [draggingIndex, setDraggingIndex, setDraggingIndexImmi] =
64        useDelayFalsy<number | undefined>(undefined, 2000);
65    const musicState = TrackPlayer.useMusicState();
66
67    const [layout, setLayout] = useState<LayoutRectangle>();
68
69    const listRef = useRef<FlatList<IParsedLrcItem> | null>();
70
71    const currentMusicItem = TrackPlayer.useCurrentMusic();
72    const associateMusicItem = currentMusicItem
73        ? MediaExtra.get(currentMusicItem)?.associatedLrc
74        : null;
75    // 是否展示拖拽
76    const dragShownRef = useRef(false);
77
78    // 组件是否挂载
79    const isMountedRef = useRef(true);
80
81    // 用来缓存高度
82    const itemHeightsRef = useRef<IItemHeights>({});
83
84    // 设置空白组件,获取组件高度
85    const blankComponent = useMemo(() => {
86        return (
87            <View
88                style={styles.empty}
89                onLayout={evt => {
90                    itemHeightsRef.current.blankHeight =
91                        evt.nativeEvent.layout.height;
92                }}
93            />
94        );
95    }, []);
96
97    const handleLyricItemLayout = useCallback(
98        (index: number, height: number) => {
99            itemHeightsRef.current[index] = height;
100        },
101        [],
102    );
103
104    // 滚到当前item
105    const scrollToCurrentLrcItem = useCallback(() => {
106        if (!listRef.current) {
107            return;
108        }
109        const currentLrcItem = LyricManager.getCurrentLyric();
110        const lyrics = LyricManager.getLyricState().lyrics;
111        if (currentLrcItem?.index === -1 || !currentLrcItem) {
112            listRef.current?.scrollToIndex({
113                index: 0,
114                viewPosition: 0.5,
115            });
116        } else {
117            listRef.current?.scrollToIndex({
118                index: Math.min(currentLrcItem.index ?? 0, lyrics.length - 1),
119                viewPosition: 0.5,
120            });
121        }
122    }, []);
123
124    const delayedScrollToCurrentLrcItem = useMemo(() => {
125        let sto: number;
126
127        return () => {
128            if (sto) {
129                clearTimeout(sto);
130            }
131            sto = setTimeout(() => {
132                if (isMountedRef.current) {
133                    scrollToCurrentLrcItem();
134                }
135            }, 200) as any;
136        };
137    }, []);
138
139    useEffect(() => {
140        // 暂停且拖拽才返回
141        if (
142            lyrics.length === 0 ||
143            draggingIndex !== undefined ||
144            (draggingIndex === undefined && musicIsPaused(musicState)) ||
145            lyrics[lyrics.length - 1].time < 1
146        ) {
147            return;
148        }
149        if (currentLrcItem?.index === -1 || !currentLrcItem) {
150            listRef.current?.scrollToIndex({
151                index: 0,
152                viewPosition: 0.5,
153            });
154        } else {
155            listRef.current?.scrollToIndex({
156                index: Math.min(currentLrcItem.index ?? 0, lyrics.length - 1),
157                viewPosition: 0.5,
158            });
159        }
160        // 音乐暂停状态不应该影响到滑动,所以不放在依赖里,但是这样写不好。。
161    }, [currentLrcItem, lyrics, draggingIndex]);
162
163    useEffect(() => {
164        scrollToCurrentLrcItem();
165        return () => {
166            isMountedRef.current = false;
167        };
168    }, []);
169
170    // 开始滚动时拖拽生效
171    const onScrollBeginDrag = () => {
172        dragShownRef.current = true;
173    };
174
175    const onScrollEndDrag = async () => {
176        if (draggingIndex !== undefined) {
177            setDraggingIndex(undefined);
178        }
179        dragShownRef.current = false;
180    };
181
182    const onScroll = (e: any) => {
183        if (dragShownRef.current) {
184            const offset =
185                e.nativeEvent.contentOffset.y +
186                e.nativeEvent.layoutMeasurement.height / 2;
187
188            const itemHeights = itemHeightsRef.current;
189            let height = itemHeights.blankHeight!;
190            if (offset <= height) {
191                setDraggingIndex(0);
192                return;
193            }
194            for (let i = 0; i < lyrics.length; ++i) {
195                height += itemHeights[i] ?? 0;
196                if (height > offset) {
197                    setDraggingIndex(i);
198                    return;
199                }
200            }
201        }
202    };
203
204    const onLyricSeekPress = async () => {
205        if (draggingIndex !== undefined) {
206            const time = lyrics[draggingIndex].time + +(meta?.offset ?? 0);
207            if (time !== undefined && !isNaN(time)) {
208                await TrackPlayer.seekTo(time);
209                await TrackPlayer.play();
210                setDraggingIndexImmi(undefined);
211            }
212        }
213    };
214
215    const tapGesture = Gesture.Tap()
216        .onStart(() => {
217            onTurnPageClick?.();
218        })
219        .runOnJS(true);
220
221    const unlinkTapGesture = Gesture.Tap()
222        .onStart(() => {
223            if (currentMusicItem) {
224                MediaExtra.update(currentMusicItem, {
225                    associatedLrc: undefined,
226                });
227                LyricManager.refreshLyric(false, true);
228            }
229        })
230        .runOnJS(true);
231
232    return (
233        <>
234            <GestureDetector gesture={tapGesture}>
235                <View style={globalStyle.fwflex1}>
236                    {loading ? (
237                        <Loading color="white" />
238                    ) : lyrics?.length ? (
239                        <FlatList
240                            ref={_ => {
241                                listRef.current = _;
242                            }}
243                            onLayout={e => {
244                                setLayout(e.nativeEvent.layout);
245                            }}
246                            viewabilityConfig={{
247                                itemVisiblePercentThreshold: 100,
248                            }}
249                            onScrollToIndexFailed={({index}) => {
250                                delay(120).then(() => {
251                                    listRef.current?.scrollToIndex({
252                                        index: Math.min(
253                                            index ?? 0,
254                                            lyrics.length - 1,
255                                        ),
256                                        viewPosition: 0.5,
257                                    });
258                                });
259                            }}
260                            fadingEdgeLength={120}
261                            ListHeaderComponent={
262                                <>
263                                    {blankComponent}
264                                    <View style={styles.lyricMeta}>
265                                        {associateMusicItem ? (
266                                            <>
267                                                <Text
268                                                    style={[
269                                                        styles.lyricMetaText,
270                                                        fontSizeStyle,
271                                                    ]}
272                                                    ellipsizeMode="middle"
273                                                    numberOfLines={1}>
274                                                    歌词关联自「
275                                                    {
276                                                        associateMusicItem.platform
277                                                    }{' '}
278                                                    -{' '}
279                                                    {associateMusicItem.title ||
280                                                        ''}
281282                                                </Text>
283
284                                                <GestureDetector
285                                                    gesture={unlinkTapGesture}>
286                                                    <Text
287                                                        style={[
288                                                            styles.linkText,
289                                                            fontSizeStyle,
290                                                        ]}>
291                                                        解除关联
292                                                    </Text>
293                                                </GestureDetector>
294                                            </>
295                                        ) : null}
296                                    </View>
297                                </>
298                            }
299                            ListFooterComponent={blankComponent}
300                            onScrollBeginDrag={onScrollBeginDrag}
301                            onMomentumScrollEnd={onScrollEndDrag}
302                            onScroll={onScroll}
303                            scrollEventThrottle={32}
304                            style={styles.wrapper}
305                            data={lyrics}
306                            initialNumToRender={30}
307                            overScrollMode="never"
308                            extraData={currentLrcItem}
309                            renderItem={({item, index}) => {
310                                let text = item.lrc;
311                                if (showTranslation && hasTranslation) {
312                                    text += `\n${item?.translation ?? ''}`;
313                                }
314
315                                return (
316                                    <LyricItemComponent
317                                        index={index}
318                                        text={text}
319                                        fontSize={fontSizeStyle.fontSize}
320                                        onLayout={handleLyricItemLayout}
321                                        light={draggingIndex === index}
322                                        highlight={
323                                            currentLrcItem?.index === index
324                                        }
325                                    />
326                                );
327                            }}
328                        />
329                    ) : (
330                        <View style={globalStyle.fullCenter}>
331                            <Text style={[styles.white, fontSizeStyle]}>
332                                暂无歌词
333                            </Text>
334                            <TapGestureHandler
335                                onActivated={() => {
336                                    showPanel('SearchLrc', {
337                                        musicItem:
338                                            TrackPlayer.getCurrentMusic(),
339                                    });
340                                }}>
341                                <Text
342                                    style={[styles.searchLyric, fontSizeStyle]}>
343                                    搜索歌词
344                                </Text>
345                            </TapGestureHandler>
346                        </View>
347                    )}
348                    {draggingIndex !== undefined && (
349                        <View
350                            style={[
351                                styles.draggingTime,
352                                layout?.height
353                                    ? {
354                                          top:
355                                              (layout.height - ITEM_HEIGHT) / 2,
356                                      }
357                                    : null,
358                            ]}>
359                            <DraggingTime
360                                time={
361                                    (lyrics[draggingIndex]?.time ?? 0) +
362                                    +(meta?.offset ?? 0)
363                                }
364                            />
365                            <View style={styles.singleLine} />
366
367                            <IconButtonWithGesture
368                                style={styles.playIcon}
369                                sizeType="small"
370                                name="play"
371                                onPress={onLyricSeekPress}
372                            />
373                        </View>
374                    )}
375                </View>
376            </GestureDetector>
377            <LyricOperations
378                scrollToCurrentLrcItem={delayedScrollToCurrentLrcItem}
379            />
380        </>
381    );
382}
383
384const styles = StyleSheet.create({
385    wrapper: {
386        width: '100%',
387        marginVertical: rpx(48),
388        flex: 1,
389    },
390    empty: {
391        paddingTop: '70%',
392    },
393    white: {
394        color: 'white',
395    },
396    lyricMeta: {
397        position: 'absolute',
398        width: '100%',
399        flexDirection: 'row',
400        justifyContent: 'center',
401        alignItems: 'center',
402        left: 0,
403        paddingHorizontal: rpx(48),
404        bottom: rpx(48),
405    },
406    lyricMetaText: {
407        color: 'white',
408        opacity: 0.8,
409        maxWidth: '80%',
410    },
411    linkText: {
412        color: '#66ccff',
413        textDecorationLine: 'underline',
414    },
415    draggingTime: {
416        position: 'absolute',
417        width: '100%',
418        height: ITEM_HEIGHT,
419        top: '40%',
420        marginTop: rpx(48),
421        paddingHorizontal: rpx(18),
422        right: 0,
423        flexDirection: 'row',
424        alignItems: 'center',
425        justifyContent: 'space-between',
426    },
427    draggingTimeText: {
428        color: '#dddddd',
429        fontSize: fontSizeConst.description,
430        width: rpx(90),
431    },
432    singleLine: {
433        width: '67%',
434        height: 1,
435        backgroundColor: '#cccccc',
436        opacity: 0.4,
437    },
438    playIcon: {
439        width: rpx(90),
440        textAlign: 'right',
441        color: 'white',
442    },
443    searchLyric: {
444        width: rpx(180),
445        marginTop: rpx(14),
446        paddingVertical: rpx(10),
447        textAlign: 'center',
448        alignSelf: 'center',
449        color: '#66eeff',
450        textDecorationLine: 'underline',
451    },
452});
453