mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-05-23 22:37:41 +08:00
新增是否允许通过歌词调整播放进度功能
This commit is contained in:
parent
cc5628252c
commit
3d735273e6
@ -12,6 +12,7 @@
|
||||
|
||||
- 新增列表设置-是否显示歌曲专辑名,默认关闭
|
||||
- 新增列表设置-是否显示歌曲时长,默认开启
|
||||
- 新增是否允许通过歌词调整播放进度功能,默认关闭,可到播放详情页右上角设置开启
|
||||
|
||||
### 优化
|
||||
|
||||
|
@ -31,6 +31,7 @@ const defaultSetting: LX.AppSetting = {
|
||||
'playDetail.style.align': 'center',
|
||||
'playDetail.vertical.style.lrcFontSize': 176,
|
||||
'playDetail.horizontal.style.lrcFontSize': 180,
|
||||
'playDetail.isShowLyricProgressSetting': false,
|
||||
|
||||
'desktopLyric.enable': false,
|
||||
'desktopLyric.isLock': false,
|
||||
|
@ -115,6 +115,7 @@
|
||||
"play_detail_setting_lrc_font_size": "Lyric font size",
|
||||
"play_detail_setting_playback_rate": "Playback rate",
|
||||
"play_detail_setting_playback_rate_reset": "reset",
|
||||
"play_detail_setting_show_lyric_progress_setting": "Allows to adjust playback progress by lyrics",
|
||||
"play_detail_setting_title": "Player settings",
|
||||
"play_detail_setting_volume": "Volume",
|
||||
"play_detail_todo_tip": "What do you want? No, this function has not been implemented yet 😛, But you can try to locate the currently playing song by long pressing (only valid for playing songs in \"My List\")",
|
||||
|
@ -115,6 +115,7 @@
|
||||
"play_detail_setting_lrc_font_size": "歌词字体大小",
|
||||
"play_detail_setting_playback_rate": "播放速率",
|
||||
"play_detail_setting_playback_rate_reset": "重置",
|
||||
"play_detail_setting_show_lyric_progress_setting": "允许通过歌词调整播放进度",
|
||||
"play_detail_setting_title": "播放器设置",
|
||||
"play_detail_setting_volume": "音量大小",
|
||||
"play_detail_todo_tip": "你想干嘛?不可以的,这个功能还没有实现哦😛,不过你可以试着长按来定位当前播放的歌曲(仅对播放“我的列表”里的歌曲有效哦)",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { memo, useMemo, useEffect, useRef } from 'react'
|
||||
import { View, FlatList, type FlatListProps } from 'react-native'
|
||||
import { memo, useMemo, useEffect, useRef, useCallback } from 'react'
|
||||
import { View, FlatList, type FlatListProps, type NativeSyntheticEvent, type NativeScrollEvent, type LayoutChangeEvent } from 'react-native'
|
||||
// import { useLayout } from '@/utils/hooks'
|
||||
import { type Line, useLrcPlay, useLrcSet } from '@/plugins/lyric'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
@ -9,18 +9,21 @@ import { useSettingValue } from '@/store/setting/hook'
|
||||
import { AnimatedColorText } from '@/components/common/Text'
|
||||
import { setSpText } from '@/utils/pixelRatio'
|
||||
import playerState from '@/store/player/state'
|
||||
import { scrollTo } from '@/utils/scroll'
|
||||
import PlayLine, { type PlayLineType } from '../components/PlayLine'
|
||||
// import { screenkeepAwake } from '@/utils/nativeModules/utils'
|
||||
// import { log } from '@/utils/log'
|
||||
// import { toast } from '@/utils/tools'
|
||||
|
||||
type FlatListType = FlatListProps<Line>
|
||||
|
||||
|
||||
const LrcLine = memo(({ line, lineNum, activeLine }: {
|
||||
interface LineProps {
|
||||
line: Line
|
||||
lineNum: number
|
||||
activeLine: number
|
||||
}) => {
|
||||
onLayout: (lineNum: number, height: number, width: number) => void
|
||||
}
|
||||
const LrcLine = memo(({ line, lineNum, activeLine, onLayout }: LineProps) => {
|
||||
const theme = useTheme()
|
||||
const lrcFontSize = useSettingValue('playDetail.horizontal.style.lrcFontSize')
|
||||
const textAlign = useSettingValue('playDetail.style.align')
|
||||
@ -38,10 +41,14 @@ const LrcLine = memo(({ line, lineNum, activeLine }: {
|
||||
]
|
||||
}, [activeLine, lineNum, theme])
|
||||
|
||||
const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => {
|
||||
onLayout(lineNum, nativeEvent.layout.height, nativeEvent.layout.width)
|
||||
}
|
||||
|
||||
// textBreakStrategy="simple" 用于解决某些设备上字体被截断的问题
|
||||
// https://stackoverflow.com/a/72822360
|
||||
return (
|
||||
<View style={styles.line}>
|
||||
<View style={styles.line} onLayout={handleLayout}>
|
||||
<AnimatedColorText style={{
|
||||
...styles.lineText,
|
||||
textAlign,
|
||||
@ -69,11 +76,16 @@ export default () => {
|
||||
const lyricLines = useLrcSet()
|
||||
const { line } = useLrcPlay()
|
||||
const flatListRef = useRef<FlatList>(null)
|
||||
const playLineRef = useRef<PlayLineType>(null)
|
||||
const isPauseScrollRef = useRef(true)
|
||||
const scrollTimoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const delayScrollTimeout = useRef<NodeJS.Timeout | null>(null)
|
||||
const lineRef = useRef(0)
|
||||
const lineRef = useRef({ line: 0, prevLine: 0 })
|
||||
const isFirstSetLrc = useRef(true)
|
||||
const scrollInfoRef = useRef<NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'] | null>(null)
|
||||
const listLayoutInfoRef = useRef<{ spaceHeight: number, lineHeights: number[] }>({ spaceHeight: 0, lineHeights: [] })
|
||||
const scrollCancelRef = useRef<(() => void) | null>(null)
|
||||
const isShowLyricProgressSetting = useSettingValue('playDetail.isShowLyricProgressSetting')
|
||||
// useLock()
|
||||
// const [imgUrl, setImgUrl] = useState(null)
|
||||
// const theme = useGetter('common', 'theme')
|
||||
@ -87,21 +99,45 @@ export default () => {
|
||||
// }, [playMusicInfo])
|
||||
|
||||
// const imgWidth = useMemo(() => layout.width * 0.75, [layout.width])
|
||||
const handleScrollToActive = (index = lineRef.current) => {
|
||||
const handleScrollToActive = (index = lineRef.current.line) => {
|
||||
if (index < 0) return
|
||||
if (flatListRef.current) {
|
||||
try {
|
||||
flatListRef.current.scrollToIndex({
|
||||
index,
|
||||
animated: true,
|
||||
viewPosition: 0.4,
|
||||
})
|
||||
} catch {}
|
||||
// console.log('handleScrollToActive', index)
|
||||
if (scrollCancelRef.current) {
|
||||
scrollCancelRef.current()
|
||||
scrollCancelRef.current = null
|
||||
}
|
||||
if (scrollInfoRef.current && lineRef.current.line - lineRef.current.prevLine == 1) {
|
||||
let offset = listLayoutInfoRef.current.spaceHeight
|
||||
for (let line = 0; line < index; line++) {
|
||||
offset += listLayoutInfoRef.current.lineHeights[line]
|
||||
}
|
||||
try {
|
||||
scrollCancelRef.current = scrollTo(flatListRef.current, scrollInfoRef.current, offset - scrollInfoRef.current.layoutMeasurement.height * 0.36, 300, () => {
|
||||
scrollCancelRef.current = null
|
||||
})
|
||||
} catch {}
|
||||
} else {
|
||||
try {
|
||||
flatListRef.current.scrollToIndex({
|
||||
index,
|
||||
animated: true,
|
||||
viewPosition: 0.4,
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
scrollInfoRef.current = nativeEvent
|
||||
if (isPauseScrollRef.current) {
|
||||
playLineRef.current?.updateScrollInfo(nativeEvent)
|
||||
}
|
||||
}
|
||||
const handleScrollBeginDrag = () => {
|
||||
isPauseScrollRef.current = true
|
||||
playLineRef.current?.setVisible(true)
|
||||
if (delayScrollTimeout.current) {
|
||||
clearTimeout(delayScrollTimeout.current)
|
||||
delayScrollTimeout.current = null
|
||||
@ -116,6 +152,7 @@ export default () => {
|
||||
if (!isPauseScrollRef.current) return
|
||||
if (scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current)
|
||||
scrollTimoutRef.current = setTimeout(() => {
|
||||
playLineRef.current?.setVisible(false)
|
||||
scrollTimoutRef.current = null
|
||||
isPauseScrollRef.current = false
|
||||
if (!playerState.isPlay) return
|
||||
@ -139,25 +176,43 @@ export default () => {
|
||||
|
||||
useEffect(() => {
|
||||
// linesRef.current = lyricLines
|
||||
listLayoutInfoRef.current.lineHeights = []
|
||||
lineRef.current.prevLine = 0
|
||||
lineRef.current.line = 0
|
||||
if (!flatListRef.current) return
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: 0,
|
||||
animated: false,
|
||||
})
|
||||
if (isFirstSetLrc.current) {
|
||||
isFirstSetLrc.current = false
|
||||
setTimeout(() => {
|
||||
isPauseScrollRef.current = false
|
||||
handleScrollToActive()
|
||||
}, 100)
|
||||
} else {
|
||||
handleScrollToActive(0)
|
||||
}
|
||||
if (!lyricLines.length) return
|
||||
playLineRef.current?.updateLyricLines(lyricLines)
|
||||
requestAnimationFrame(() => {
|
||||
if (isFirstSetLrc.current) {
|
||||
isFirstSetLrc.current = false
|
||||
setTimeout(() => {
|
||||
isPauseScrollRef.current = false
|
||||
handleScrollToActive()
|
||||
}, 100)
|
||||
} else {
|
||||
if (delayScrollTimeout.current) clearTimeout(delayScrollTimeout.current)
|
||||
delayScrollTimeout.current = setTimeout(() => {
|
||||
handleScrollToActive(0)
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
}, [lyricLines])
|
||||
|
||||
useEffect(() => {
|
||||
lineRef.current = line
|
||||
if (line < 0) return
|
||||
lineRef.current.prevLine = lineRef.current.line
|
||||
lineRef.current.line = line
|
||||
if (!flatListRef.current || isPauseScrollRef.current) return
|
||||
|
||||
if (line - lineRef.current.prevLine != 1) {
|
||||
handleScrollToActive()
|
||||
return
|
||||
}
|
||||
|
||||
delayScrollTimeout.current = setTimeout(() => {
|
||||
delayScrollTimeout.current = null
|
||||
handleScrollToActive()
|
||||
@ -165,39 +220,57 @@ export default () => {
|
||||
}, [line])
|
||||
|
||||
const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => {
|
||||
// console.log(info)
|
||||
void wait().then(() => {
|
||||
handleScrollToActive(info.index)
|
||||
})
|
||||
}
|
||||
|
||||
const handleLineLayout = useCallback<LineProps['onLayout']>((lineNum, height) => {
|
||||
listLayoutInfoRef.current.lineHeights[lineNum] = height
|
||||
playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)
|
||||
}, [])
|
||||
|
||||
const handleSpaceLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
|
||||
listLayoutInfoRef.current.spaceHeight = nativeEvent.layout.height
|
||||
playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)
|
||||
}, [])
|
||||
|
||||
const handlePlayLine = useCallback((time: number) => {
|
||||
playLineRef.current?.setVisible(false)
|
||||
global.app_event.setProgress(time)
|
||||
}, [])
|
||||
|
||||
const renderItem: FlatListType['renderItem'] = ({ item, index }) => {
|
||||
return (
|
||||
<LrcLine line={item} lineNum={index} activeLine={line} />
|
||||
<LrcLine line={item} lineNum={index} activeLine={line} onLayout={handleLineLayout} />
|
||||
)
|
||||
}
|
||||
const getkey: FlatListType['keyExtractor'] = (item, index) => `${index}${item.text}`
|
||||
|
||||
const spaceComponent = useMemo(() => (
|
||||
<View style={styles.space}></View>
|
||||
), [])
|
||||
<View style={styles.space} onLayout={handleSpaceLayout}></View>
|
||||
), [handleSpaceLayout])
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={lyricLines}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={getkey}
|
||||
style={styles.container}
|
||||
ref={flatListRef}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={spaceComponent}
|
||||
ListFooterComponent={spaceComponent}
|
||||
onScrollBeginDrag={handleScrollBeginDrag}
|
||||
onScrollEndDrag={onScrollEndDrag}
|
||||
fadingEdgeLength={100}
|
||||
initialNumToRender={Math.max(line + 10, 10)}
|
||||
onScrollToIndexFailed={handleScrollToIndexFailed}
|
||||
/>
|
||||
<>
|
||||
<FlatList
|
||||
data={lyricLines}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={getkey}
|
||||
style={styles.container}
|
||||
ref={flatListRef}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={spaceComponent}
|
||||
ListFooterComponent={spaceComponent}
|
||||
onScrollBeginDrag={handleScrollBeginDrag}
|
||||
onScrollEndDrag={onScrollEndDrag}
|
||||
fadingEdgeLength={100}
|
||||
initialNumToRender={Math.max(line + 10, 10)}
|
||||
onScrollToIndexFailed={handleScrollToIndexFailed}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
{ isShowLyricProgressSetting ? <PlayLine ref={playLineRef} onPlayLine={handlePlayLine} /> : null }
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { memo, useMemo, useEffect, useRef } from 'react'
|
||||
import { View, FlatList, type FlatListProps } from 'react-native'
|
||||
import { memo, useMemo, useEffect, useRef, useCallback } from 'react'
|
||||
import { View, FlatList, type FlatListProps, type LayoutChangeEvent, type NativeSyntheticEvent, type NativeScrollEvent } from 'react-native'
|
||||
// import { useLayout } from '@/utils/hooks'
|
||||
import { type Line, useLrcPlay, useLrcSet } from '@/plugins/lyric'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
@ -9,6 +9,8 @@ import { useSettingValue } from '@/store/setting/hook'
|
||||
import { AnimatedColorText } from '@/components/common/Text'
|
||||
import { setSpText } from '@/utils/pixelRatio'
|
||||
import playerState from '@/store/player/state'
|
||||
import { scrollTo } from '@/utils/scroll'
|
||||
import PlayLine, { type PlayLineType } from '../components/PlayLine'
|
||||
// import { screenkeepAwake } from '@/utils/nativeModules/utils'
|
||||
// import { log } from '@/utils/log'
|
||||
// import { toast } from '@/utils/tools'
|
||||
@ -54,11 +56,13 @@ type FlatListType = FlatListProps<Line>
|
||||
// }, [])
|
||||
// }
|
||||
|
||||
const LrcLine = memo(({ line, lineNum, activeLine }: {
|
||||
interface LineProps {
|
||||
line: Line
|
||||
lineNum: number
|
||||
activeLine: number
|
||||
}) => {
|
||||
onLayout: (lineNum: number, height: number, width: number) => void
|
||||
}
|
||||
const LrcLine = memo(({ line, lineNum, activeLine, onLayout }: LineProps) => {
|
||||
const theme = useTheme()
|
||||
const lrcFontSize = useSettingValue('playDetail.vertical.style.lrcFontSize')
|
||||
const textAlign = useSettingValue('playDetail.style.align')
|
||||
@ -76,11 +80,15 @@ const LrcLine = memo(({ line, lineNum, activeLine }: {
|
||||
]
|
||||
}, [activeLine, lineNum, theme])
|
||||
|
||||
const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => {
|
||||
onLayout(lineNum, nativeEvent.layout.height, nativeEvent.layout.width)
|
||||
}
|
||||
|
||||
|
||||
// textBreakStrategy="simple" 用于解决某些设备上字体被截断的问题
|
||||
// https://stackoverflow.com/a/72822360
|
||||
return (
|
||||
<View style={styles.line}>
|
||||
<View style={styles.line} onLayout={handleLayout}>
|
||||
<AnimatedColorText style={{
|
||||
...styles.lineText,
|
||||
textAlign,
|
||||
@ -108,11 +116,16 @@ export default () => {
|
||||
const lyricLines = useLrcSet()
|
||||
const { line } = useLrcPlay()
|
||||
const flatListRef = useRef<FlatList>(null)
|
||||
const playLineRef = useRef<PlayLineType>(null)
|
||||
const isPauseScrollRef = useRef(true)
|
||||
const scrollTimoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const delayScrollTimeout = useRef<NodeJS.Timeout | null>(null)
|
||||
const lineRef = useRef(0)
|
||||
const lineRef = useRef({ line: 0, prevLine: 0 })
|
||||
const isFirstSetLrc = useRef(true)
|
||||
const scrollInfoRef = useRef<NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'] | null>(null)
|
||||
const listLayoutInfoRef = useRef<{ spaceHeight: number, lineHeights: number[] }>({ spaceHeight: 0, lineHeights: [] })
|
||||
const scrollCancelRef = useRef<(() => void) | null>(null)
|
||||
const isShowLyricProgressSetting = useSettingValue('playDetail.isShowLyricProgressSetting')
|
||||
// useLock()
|
||||
// const [imgUrl, setImgUrl] = useState(null)
|
||||
// const theme = useGetter('common', 'theme')
|
||||
@ -126,21 +139,45 @@ export default () => {
|
||||
// }, [playMusicInfo])
|
||||
|
||||
// const imgWidth = useMemo(() => layout.width * 0.75, [layout.width])
|
||||
const handleScrollToActive = (index = lineRef.current) => {
|
||||
const handleScrollToActive = (index = lineRef.current.line) => {
|
||||
if (index < 0) return
|
||||
if (flatListRef.current) {
|
||||
try {
|
||||
flatListRef.current.scrollToIndex({
|
||||
index,
|
||||
animated: true,
|
||||
viewPosition: 0.4,
|
||||
})
|
||||
} catch {}
|
||||
// console.log('handleScrollToActive', index)
|
||||
if (scrollCancelRef.current) {
|
||||
scrollCancelRef.current()
|
||||
scrollCancelRef.current = null
|
||||
}
|
||||
if (scrollInfoRef.current && lineRef.current.line - lineRef.current.prevLine == 1) {
|
||||
let offset = listLayoutInfoRef.current.spaceHeight
|
||||
for (let line = 0; line < index; line++) {
|
||||
offset += listLayoutInfoRef.current.lineHeights[line]
|
||||
}
|
||||
try {
|
||||
scrollCancelRef.current = scrollTo(flatListRef.current, scrollInfoRef.current, offset - scrollInfoRef.current.layoutMeasurement.height * 0.36, 300, () => {
|
||||
scrollCancelRef.current = null
|
||||
})
|
||||
} catch {}
|
||||
} else {
|
||||
try {
|
||||
flatListRef.current.scrollToIndex({
|
||||
index,
|
||||
animated: true,
|
||||
viewPosition: 0.4,
|
||||
})
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
scrollInfoRef.current = nativeEvent
|
||||
if (isPauseScrollRef.current) {
|
||||
playLineRef.current?.updateScrollInfo(nativeEvent)
|
||||
}
|
||||
}
|
||||
const handleScrollBeginDrag = () => {
|
||||
isPauseScrollRef.current = true
|
||||
playLineRef.current?.setVisible(true)
|
||||
if (delayScrollTimeout.current) {
|
||||
clearTimeout(delayScrollTimeout.current)
|
||||
delayScrollTimeout.current = null
|
||||
@ -155,6 +192,7 @@ export default () => {
|
||||
if (!isPauseScrollRef.current) return
|
||||
if (scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current)
|
||||
scrollTimoutRef.current = setTimeout(() => {
|
||||
playLineRef.current?.setVisible(false)
|
||||
scrollTimoutRef.current = null
|
||||
isPauseScrollRef.current = false
|
||||
if (!playerState.isPlay) return
|
||||
@ -178,25 +216,43 @@ export default () => {
|
||||
|
||||
useEffect(() => {
|
||||
// linesRef.current = lyricLines
|
||||
listLayoutInfoRef.current.lineHeights = []
|
||||
lineRef.current.prevLine = 0
|
||||
lineRef.current.line = 0
|
||||
if (!flatListRef.current) return
|
||||
flatListRef.current.scrollToOffset({
|
||||
offset: 0,
|
||||
animated: false,
|
||||
})
|
||||
if (isFirstSetLrc.current) {
|
||||
isFirstSetLrc.current = false
|
||||
setTimeout(() => {
|
||||
isPauseScrollRef.current = false
|
||||
handleScrollToActive()
|
||||
}, 100)
|
||||
} else {
|
||||
handleScrollToActive(0)
|
||||
}
|
||||
if (!lyricLines.length) return
|
||||
playLineRef.current?.updateLyricLines(lyricLines)
|
||||
requestAnimationFrame(() => {
|
||||
if (isFirstSetLrc.current) {
|
||||
isFirstSetLrc.current = false
|
||||
setTimeout(() => {
|
||||
isPauseScrollRef.current = false
|
||||
handleScrollToActive()
|
||||
}, 100)
|
||||
} else {
|
||||
if (delayScrollTimeout.current) clearTimeout(delayScrollTimeout.current)
|
||||
delayScrollTimeout.current = setTimeout(() => {
|
||||
handleScrollToActive(0)
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
}, [lyricLines])
|
||||
|
||||
useEffect(() => {
|
||||
lineRef.current = line
|
||||
if (line < 0) return
|
||||
lineRef.current.prevLine = lineRef.current.line
|
||||
lineRef.current.line = line
|
||||
if (!flatListRef.current || isPauseScrollRef.current) return
|
||||
|
||||
if (line - lineRef.current.prevLine != 1) {
|
||||
handleScrollToActive()
|
||||
return
|
||||
}
|
||||
|
||||
delayScrollTimeout.current = setTimeout(() => {
|
||||
delayScrollTimeout.current = null
|
||||
handleScrollToActive()
|
||||
@ -204,39 +260,57 @@ export default () => {
|
||||
}, [line])
|
||||
|
||||
const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => {
|
||||
// console.log(info)
|
||||
void wait().then(() => {
|
||||
handleScrollToActive(info.index)
|
||||
})
|
||||
}
|
||||
|
||||
const handleLineLayout = useCallback<LineProps['onLayout']>((lineNum, height) => {
|
||||
listLayoutInfoRef.current.lineHeights[lineNum] = height
|
||||
playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)
|
||||
}, [])
|
||||
|
||||
const handleSpaceLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {
|
||||
listLayoutInfoRef.current.spaceHeight = nativeEvent.layout.height
|
||||
playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)
|
||||
}, [])
|
||||
|
||||
const handlePlayLine = useCallback((time: number) => {
|
||||
playLineRef.current?.setVisible(false)
|
||||
global.app_event.setProgress(time)
|
||||
}, [])
|
||||
|
||||
const renderItem: FlatListType['renderItem'] = ({ item, index }) => {
|
||||
return (
|
||||
<LrcLine line={item} lineNum={index} activeLine={line} />
|
||||
<LrcLine line={item} lineNum={index} activeLine={line} onLayout={handleLineLayout} />
|
||||
)
|
||||
}
|
||||
const getkey: FlatListType['keyExtractor'] = (item, index) => `${index}${item.text}`
|
||||
|
||||
const spaceComponent = useMemo(() => (
|
||||
<View style={styles.space}></View>
|
||||
), [])
|
||||
<View style={styles.space} onLayout={handleSpaceLayout}></View>
|
||||
), [handleSpaceLayout])
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={lyricLines}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={getkey}
|
||||
style={styles.container}
|
||||
ref={flatListRef}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={spaceComponent}
|
||||
ListFooterComponent={spaceComponent}
|
||||
onScrollBeginDrag={handleScrollBeginDrag}
|
||||
onScrollEndDrag={onScrollEndDrag}
|
||||
fadingEdgeLength={100}
|
||||
initialNumToRender={Math.max(line + 10, 10)}
|
||||
onScrollToIndexFailed={handleScrollToIndexFailed}
|
||||
/>
|
||||
<>
|
||||
<FlatList
|
||||
data={lyricLines}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={getkey}
|
||||
style={styles.container}
|
||||
ref={flatListRef}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={spaceComponent}
|
||||
ListFooterComponent={spaceComponent}
|
||||
onScrollBeginDrag={handleScrollBeginDrag}
|
||||
onScrollEndDrag={onScrollEndDrag}
|
||||
fadingEdgeLength={100}
|
||||
initialNumToRender={Math.max(line + 10, 10)}
|
||||
onScrollToIndexFailed={handleScrollToIndexFailed}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
{ isShowLyricProgressSetting ? <PlayLine ref={playLineRef} onPlayLine={handlePlayLine} /> : null }
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -251,8 +325,8 @@ const styles = createStyle({
|
||||
paddingTop: '80%',
|
||||
},
|
||||
line: {
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
// opacity: 0,
|
||||
},
|
||||
lineText: {
|
||||
|
136
src/screens/PlayDetail/components/PlayLine.tsx
Normal file
136
src/screens/PlayDetail/components/PlayLine.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { type NativeScrollEvent, type NativeSyntheticEvent, View, TouchableOpacity, Animated } from 'react-native'
|
||||
import Text from '@/components/common/Text'
|
||||
import { createStyle } from '@/utils/tools'
|
||||
import { type Lines } from 'lrc-file-parser'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { BorderWidths } from '@/theme'
|
||||
import { formatPlayTime2 } from '@/utils'
|
||||
import { Icon } from '@/components/common/Icon'
|
||||
|
||||
|
||||
export interface PlayLineType {
|
||||
updateScrollInfo: (scrollInfo: NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'] | null) => void
|
||||
updateLayoutInfo: (listLayoutInfo: { spaceHeight: number, lineHeights: number[] }) => void
|
||||
updateLyricLines: (lyricLines: Lines) => void
|
||||
setVisible: (visible: boolean) => void
|
||||
}
|
||||
|
||||
export interface PlayLineProps {
|
||||
onPlayLine: (time: number) => void
|
||||
}
|
||||
|
||||
const ANIMATION_DURATION = 300
|
||||
|
||||
export default forwardRef<PlayLineType, PlayLineProps>(({ onPlayLine }, ref) => {
|
||||
const theme = useTheme()
|
||||
const [scrollInfo, setScrollInfo] = useState<NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'] | null>(null)
|
||||
const [listLayoutInfo, setListLayoutInfo] = useState<{ spaceHeight: number, lineHeights: number[] }>({ spaceHeight: 0, lineHeights: [] })
|
||||
const [lyricLines, setLyricLines] = useState<Lines>([])
|
||||
const [visible, setVisible] = useState(false)
|
||||
const opsAnim = useRef<Animated.Value>(
|
||||
new Animated.Value(0),
|
||||
).current
|
||||
|
||||
const setShow = (visible: boolean) => {
|
||||
Animated.timing(opsAnim, {
|
||||
toValue: visible ? 1 : 0,
|
||||
duration: ANIMATION_DURATION,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
if (!visible) setVisible(false)
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
updateScrollInfo(scrollInfo) {
|
||||
setScrollInfo(scrollInfo)
|
||||
},
|
||||
updateLayoutInfo(listLayoutInfo) {
|
||||
setListLayoutInfo(listLayoutInfo)
|
||||
},
|
||||
updateLyricLines(lyricLines) {
|
||||
setLyricLines(lyricLines)
|
||||
},
|
||||
setVisible(visible) {
|
||||
if (visible) {
|
||||
setVisible(true)
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
setShow(visible)
|
||||
})
|
||||
// setVisible()
|
||||
},
|
||||
}), [])
|
||||
|
||||
const handlePlayLine = () => {
|
||||
onPlayLine(time / 1000)
|
||||
}
|
||||
|
||||
if (!scrollInfo || !visible) return null
|
||||
const offset = scrollInfo.contentOffset.y + scrollInfo.layoutMeasurement.height * 0.4
|
||||
let lineOffset = listLayoutInfo.spaceHeight
|
||||
let targetLineNum = -1
|
||||
for (let line = 0; line < listLayoutInfo.lineHeights.length; line++) {
|
||||
lineOffset += listLayoutInfo.lineHeights[line]
|
||||
if (lineOffset < offset) continue
|
||||
targetLineNum = line
|
||||
break
|
||||
}
|
||||
if (targetLineNum == -1) targetLineNum = listLayoutInfo.lineHeights.length - 1
|
||||
const time = lyricLines[targetLineNum]?.time ?? 0
|
||||
const timeLabel = formatPlayTime2(time / 1000)
|
||||
return (
|
||||
<Animated.View style={{ ...styles.playLine, opacity: opsAnim }}>
|
||||
<Text style={styles.label} color={theme['c-primary-font']} size={13}>{timeLabel}</Text>
|
||||
<View style={styles.lineContent}>
|
||||
<View style={{ ...styles.line, borderBottomColor: theme['c-primary-alpha-300'] }} />
|
||||
<TouchableOpacity style={styles.button} onPress={handlePlayLine}>
|
||||
<Icon name="play" color={theme['c-button-font']} size={14} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = createStyle({
|
||||
playLine: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
top: '40%',
|
||||
left: 0,
|
||||
height: 2,
|
||||
// paddingTop: 5,
|
||||
// paddingBottom: 5,
|
||||
// backgroundColor: 'rgba(0,0,0,0.1)',
|
||||
},
|
||||
label: {
|
||||
position: 'absolute',
|
||||
right: 37,
|
||||
bottom: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
},
|
||||
lineContent: {
|
||||
// backgroundColor: 'rgba(0,0,0,0.1)',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: 20,
|
||||
top: -10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
},
|
||||
line: {
|
||||
marginLeft: 15,
|
||||
borderBottomWidth: BorderWidths.normal,
|
||||
borderStyle: 'dashed',
|
||||
flex: 1,
|
||||
},
|
||||
button: {
|
||||
flex: 0,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 15,
|
||||
},
|
||||
})
|
@ -3,6 +3,7 @@ import { ScrollView, View } from 'react-native'
|
||||
import Popup, { type PopupType, type PopupProps } from '@/components/common/Popup'
|
||||
import { useI18n } from '@/lang'
|
||||
|
||||
import SettingLyricProgress from './settings/SettingLyricProgress'
|
||||
import SettingVolume from './settings/SettingVolume'
|
||||
import SettingPlaybackRate from './settings/SettingPlaybackRate'
|
||||
import SettingLrcFontSize from './settings/SettingLrcFontSize'
|
||||
@ -41,6 +42,7 @@ export default forwardRef<SettingPopupType, SettingPopupProps>(({ direction, ...
|
||||
<Popup ref={popupRef} title={t('play_detail_setting_title')} {...props}>
|
||||
<ScrollView>
|
||||
<View onStartShouldSetResponder={() => true}>
|
||||
<SettingLyricProgress />
|
||||
<SettingVolume />
|
||||
<SettingPlaybackRate />
|
||||
<SettingLrcFontSize direction={direction} />
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { View } from 'react-native'
|
||||
// import Text from '@/components/common/Text'
|
||||
import { useSettingValue } from '@/store/setting/hook'
|
||||
import { updateSetting } from '@/core/common'
|
||||
import { useI18n } from '@/lang'
|
||||
import CheckBox from '@/components/common/CheckBox'
|
||||
import styles from './style'
|
||||
|
||||
|
||||
export default () => {
|
||||
const t = useI18n()
|
||||
const isShowLyricProgressSetting = useSettingValue('playDetail.isShowLyricProgressSetting')
|
||||
const setShowLyricProgressSetting = (showLyricProgressSetting: boolean) => {
|
||||
updateSetting({ 'playDetail.isShowLyricProgressSetting': showLyricProgressSetting })
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.content}>
|
||||
<CheckBox marginBottom={3} check={isShowLyricProgressSetting} label={t('play_detail_setting_show_lyric_progress_setting')} onChange={setShowLyricProgressSetting} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
)
|
||||
}
|
||||
|
5
src/types/app_setting.d.ts
vendored
5
src/types/app_setting.d.ts
vendored
@ -171,6 +171,11 @@ declare global {
|
||||
*/
|
||||
'playDetail.horizontal.style.lrcFontSize': number
|
||||
|
||||
/**
|
||||
* 播放详情页-是否允许通过歌词调整播放进度
|
||||
*/
|
||||
'playDetail.isShowLyricProgressSetting': boolean
|
||||
|
||||
/**
|
||||
* 是否启用桌面歌词
|
||||
*/
|
||||
|
93
src/utils/scroll.ts
Normal file
93
src/utils/scroll.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { type NativeScrollEvent, type FlatList, type NativeSyntheticEvent } from 'react-native'
|
||||
|
||||
const easeInOutQuad = (t: number, b: number, c: number, d: number): number => {
|
||||
t /= d / 2
|
||||
if (t < 1) return (c / 2) * t * t + b
|
||||
t--
|
||||
return (-c / 2) * (t * (t - 2) - 1) + b
|
||||
}
|
||||
|
||||
type Noop = () => void
|
||||
const noop: Noop = () => {}
|
||||
|
||||
const handleScrollY = (element: FlatList<any>, info: NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'], to: number, duration = 300, fn = noop): Noop => {
|
||||
if (element == null) {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
|
||||
const key = Math.random()
|
||||
|
||||
|
||||
const start = info.contentOffset.y
|
||||
let cancel = false
|
||||
if (to > start) {
|
||||
let maxScrollTop = info.contentSize.height - info.layoutMeasurement.height
|
||||
if (to > maxScrollTop) to = maxScrollTop
|
||||
} else if (to < start) {
|
||||
if (to < 0) to = 0
|
||||
} else {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
const change = to - start
|
||||
const increment = 10
|
||||
if (!change) {
|
||||
fn()
|
||||
return noop
|
||||
}
|
||||
|
||||
let currentTime = 0
|
||||
let val
|
||||
|
||||
const animateScroll = () => {
|
||||
// @ts-expect-error
|
||||
if (cancel || element.lx_scrollKey != key) {
|
||||
fn()
|
||||
return
|
||||
}
|
||||
currentTime += increment
|
||||
val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))
|
||||
element.scrollToOffset({ offset: val, animated: false })
|
||||
if (currentTime < duration) {
|
||||
setTimeout(animateScroll, increment)
|
||||
} else {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
// @ts-expect-error
|
||||
element.lx_scrollKey = key
|
||||
requestAnimationFrame(() => {
|
||||
animateScroll()
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancel = true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置滚动条位置
|
||||
* @param element 要设置滚动的容器 dom
|
||||
* @param to 滚动的目标位置
|
||||
* @param duration 滚动完成时间 ms
|
||||
* @param fn 滚动完成后的回调
|
||||
* @param delay 延迟执行时间
|
||||
*/
|
||||
export const scrollTo = (element: FlatList<any>, info: NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'], to: number, duration = 300, fn = () => {}, delay = 0): () => void => {
|
||||
let cancelFn: () => void
|
||||
let timeout: NodeJS.Timeout | null
|
||||
if (delay) {
|
||||
let scrollCancelFn: Noop
|
||||
cancelFn = () => {
|
||||
timeout == null ? scrollCancelFn?.() : clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
scrollCancelFn = handleScrollY(element, info, to, duration, fn)
|
||||
}, delay)
|
||||
} else {
|
||||
cancelFn = handleScrollY(element, info, to, duration, fn) ?? noop
|
||||
}
|
||||
return cancelFn
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user