Skip to content

Commit becac88

Browse files
committed
feat: add progress slider
1 parent 48baccd commit becac88

11 files changed

Lines changed: 114 additions & 45 deletions

File tree

apps/mobile/modules/player/FullPlayer.tsx

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,36 +36,6 @@ export default function FullPlayer() {
3636
const styles = StyleSheet.create({
3737
container: {
3838
flex: 1,
39-
},
40-
page: {
41-
flex: 1,
42-
justifyContent: 'center',
43-
alignItems: 'center',
44-
paddingHorizontal: 20,
45-
},
46-
pageTitle: {
47-
fontSize: 24,
48-
fontWeight: 'bold',
49-
marginBottom: 8,
50-
},
51-
pageSubtitle: {
52-
fontSize: 16,
53-
opacity: 0.7,
54-
},
55-
trackInfo: {
56-
alignItems: 'center',
57-
marginBottom: 40,
58-
width: '100%',
59-
},
60-
title: {
61-
fontSize: 24,
62-
fontWeight: 'bold',
63-
textAlign: 'center',
64-
marginBottom: 8,
65-
},
66-
artist: {
67-
fontSize: 18,
68-
textAlign: 'center',
69-
opacity: 0.7,
39+
paddingHorizontal: 28,
7040
},
7141
})

apps/mobile/modules/player/FullPlayerArtwork.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ const styles = StyleSheet.create({
2323
container: {
2424
flex: 1,
2525
justifyContent: 'center',
26-
alignItems: 'center',
2726
},
2827
artwork: {
29-
width: screenWidth - 48,
28+
width: screenWidth - 56,
3029
aspectRatio: 1,
3130
borderRadius: 16,
3231
marginBottom: 40,
@@ -37,6 +36,6 @@ const styles = StyleSheet.create({
3736
},
3837
shadowOpacity: 0.3,
3938
shadowRadius: 8,
40-
elevation: 8,
39+
elevation: 2,
4140
},
4241
})

apps/mobile/modules/player/FullPlayerControl.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import { playerController, PlayMode, usePlayerStore } from '@flow/player'
2-
import { useCallback } from 'react'
3-
import { StyleSheet, View } from 'react-native'
1+
import { formatDuration } from '@flow/core'
2+
import { playerController, PlayMode, useDisplayTrack, usePlayerStore } from '@flow/player'
3+
import Slider from '@react-native-community/slider'
4+
import { useCallback, useState } from 'react'
5+
import { Dimensions, StyleSheet, View } from 'react-native'
46
import { IconButton, Text, useTheme } from 'react-native-paper'
57
import { useSafeAreaInsets } from 'react-native-safe-area-context'
68

9+
const { width: screenWidth } = Dimensions.get('screen')
10+
711
export default function FullPlayerControl() {
812
const { bottom } = useSafeAreaInsets()
913
const { colors } = useTheme()
14+
const displayTrack = useDisplayTrack()
1015
const isPlaying = usePlayerStore.use.isPlaying()
1116
const mode = usePlayerStore.use.mode()
17+
const position = usePlayerStore.use.position()
18+
const [isSliding, setIsSliding] = useState(false)
19+
const [sliderValue, setSliderValue] = useState(0)
1220

1321
const handlePlayPause = useCallback(() => {
1422
if (isPlaying) {
@@ -27,8 +35,48 @@ export default function FullPlayerControl() {
2735
playerController.prev()
2836
}, [])
2937

38+
const handleSliderChange = useCallback((value: number) => {
39+
setSliderValue(value)
40+
}, [])
41+
42+
const handleSliderSlidingComplete = useCallback((value: number) => {
43+
setIsSliding(false)
44+
if (displayTrack?.duration && displayTrack.duration > 0) {
45+
const newPosition = value * displayTrack.duration
46+
playerController.seekTo(newPosition)
47+
}
48+
}, [displayTrack?.duration])
49+
50+
const handleSliderSlidingStart = useCallback(() => {
51+
setIsSliding(true)
52+
}, [])
53+
54+
// 更新滑块值
55+
const currentSliderValue = isSliding ? sliderValue : (displayTrack?.duration && displayTrack.duration > 0 ? position / displayTrack.duration : 0)
56+
3057
return (
3158
<View style={[styles.container, { paddingBottom: bottom + 36 }]}>
59+
<View>
60+
<Slider
61+
style={styles.slider}
62+
value={currentSliderValue}
63+
minimumTrackTintColor={colors.primary}
64+
maximumTrackTintColor={colors.outline}
65+
thumbTintColor={colors.primary}
66+
onValueChange={handleSliderChange}
67+
onSlidingComplete={handleSliderSlidingComplete}
68+
onSlidingStart={handleSliderSlidingStart}
69+
/>
70+
<View style={styles.timeTextContainer}>
71+
<Text style={[styles.timeText, { color: colors.onSurfaceVariant }]}>
72+
{formatDuration(position)}
73+
</Text>
74+
<Text style={[styles.timeText, { color: colors.onSurfaceVariant }]}>
75+
{formatDuration(displayTrack?.duration ?? 0)}
76+
</Text>
77+
</View>
78+
</View>
79+
3280
<View style={styles.controls}>
3381
<IconButton
3482
icon="skip-previous"
@@ -62,6 +110,18 @@ const styles = StyleSheet.create({
62110
container: {
63111
gap: 20,
64112
},
113+
slider: {
114+
alignSelf: 'center',
115+
width: screenWidth - 24,
116+
},
117+
timeTextContainer: {
118+
flexDirection: 'row',
119+
justifyContent: 'space-between',
120+
},
121+
timeText: {
122+
fontSize: 12,
123+
textAlign: 'center',
124+
},
65125
controls: {
66126
flexDirection: 'row',
67127
alignItems: 'center',

apps/mobile/modules/player/FullPlayerHeader.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export default function FullPlayerHeader() {
1919
const styles = StyleSheet.create({
2020
container: {
2121
marginTop: -16,
22-
paddingHorizontal: 24,
2322
},
2423
artist: {
2524
opacity: 0.7,

apps/mobile/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@flow/store": "workspace:*",
1919
"@gorhom/bottom-sheet": "^5.1.6",
2020
"@nodefinity/react-native-music-library": "0.7.1",
21+
"@react-native-community/slider": "4.5.7",
2122
"@react-navigation/bottom-tabs": "^7.3.10",
2223
"@react-navigation/drawer": "^7.3.12",
2324
"@react-navigation/elements": "^2.3.8",
@@ -50,7 +51,7 @@
5051
"react-native-safe-area-context": "5.4.0",
5152
"react-native-screens": "~4.11.1",
5253
"react-native-tab-view": "^4.1.1",
53-
"react-native-track-player": "github:dribble-njr/react-native-track-player"
54+
"react-native-track-player": "4.1.1"
5455
},
5556
"devDependencies": {
5657
"@babel/core": "^7.25.2",

packages/player/src/playerController.rn.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,5 +192,16 @@ export const playerController: PlayerController = {
192192
logger.error('syncCurrentIndex error:', error)
193193
}
194194
},
195+
196+
async seekTo(position: number) {
197+
try {
198+
await TrackPlayer.seekTo(position)
199+
const setPosition = usePlayerStore.getState().setPosition
200+
setPosition(position)
201+
}
202+
catch (error) {
203+
logger.error('seekTo error:', error)
204+
}
205+
},
195206
// #endregion
196207
}

packages/player/src/playerController.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface PlayerController {
1818
setMode: (mode: PlayMode) => void
1919

2020
syncCurrentIndex: (index: number) => void
21+
22+
seekTo: (position: number) => void
2123
}
2224

2325
export declare const playerController: PlayerController

packages/player/src/playerStore.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface PlayerStore {
1616
currentIndex: number
1717
mode: PlayMode
1818
isPlaying: boolean
19+
position: number
1920
}
2021

2122
interface PlayerStoreActions {
@@ -38,6 +39,9 @@ interface PlayerStoreActions {
3839

3940
// sync
4041
setCurrentIndex: (index: number) => void
42+
43+
// progress
44+
setPosition: (position: number) => void
4145
}
4246

4347
function shuffle<T>(array: T[]): T[] {
@@ -57,6 +61,7 @@ const playerStoreBase = create<PlayerStore & PlayerStoreActions>()(
5761
currentIndex: 0,
5862
mode: PlayMode.ORDERED,
5963
isPlaying: false,
64+
position: 0,
6065

6166
// Add a track to the queue's end
6267
addToQueue: (track) => {
@@ -203,6 +208,10 @@ const playerStoreBase = create<PlayerStore & PlayerStoreActions>()(
203208
setCurrentIndex: (index) => {
204209
set({ currentIndex: index })
205210
},
211+
212+
setPosition: (position) => {
213+
set({ position })
214+
},
206215
}),
207216
{
208217
name: 'player-store',

packages/player/src/services/playbackService.rn.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { logger } from '@flow/core'
12
import TrackPlayer, { Event } from 'react-native-track-player'
23
import { playerController } from '../playerController'
4+
import { usePlayerStore } from '../playerStore'
35

46
export async function PlaybackService() {
57
TrackPlayer.addEventListener(Event.RemotePlay, () => {
@@ -19,8 +21,16 @@ export async function PlaybackService() {
1921
})
2022

2123
TrackPlayer.addEventListener(Event.PlaybackActiveTrackChanged, (event) => {
24+
logger.info('PlaybackActiveTrackChanged', event)
2225
if (event.index !== undefined) {
2326
playerController.syncCurrentIndex(event.index)
2427
}
2528
})
29+
30+
TrackPlayer.addEventListener(Event.PlaybackProgressUpdated, (event) => {
31+
const { position } = event
32+
const setPosition = usePlayerStore.getState().setPosition
33+
34+
setPosition(position)
35+
})
2636
}

packages/player/src/services/setupService.rn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function getTrackPlayerOptions(options?: UpdateOptions) {
3131
Capability.SkipToNext,
3232
Capability.SkipToPrevious,
3333
],
34+
progressUpdateEventInterval: 1,
3435
...options,
3536
}
3637
}

0 commit comments

Comments
 (0)