Skip to content

Commit 543e0b0

Browse files
committed
feat: add player screen
1 parent 382368c commit 543e0b0

File tree

12 files changed

+809
-41
lines changed

12 files changed

+809
-41
lines changed

example/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { AppRegistry } from 'react-native';
22
import App from './src/App';
33
import { name as appName } from './app.json';
4+
import { setupAudioPro } from './src/hooks/useSetupAudio';
45

56
AppRegistry.registerComponent(appName, () => App);
7+
8+
setupAudioPro();

example/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@
1010
"build:ios": "react-native build-ios --mode Debug"
1111
},
1212
"dependencies": {
13+
"@react-native-community/slider": "^4.5.7",
1314
"@react-native-documents/picker": "^10.1.3",
15+
"@react-navigation/native": "^7.1.11",
16+
"@react-navigation/native-stack": "^7.3.16",
1417
"react": "19.0.0",
1518
"react-native": "0.79.2",
16-
"react-native-permissions": "^5.4.0"
19+
"react-native-audio-pro": "^9.9.1",
20+
"react-native-permissions": "^5.4.0",
21+
"react-native-safe-area-context": "^5.4.1",
22+
"react-native-screens": "^4.11.1"
1723
},
1824
"devDependencies": {
1925
"@babel/core": "^7.25.2",

example/src/App.tsx

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,26 @@
1-
import { StyleSheet, View, Text, StatusBar } from 'react-native';
2-
import TrackList from './TrackList';
1+
import { StatusBar } from 'react-native';
2+
import { SafeAreaProvider } from 'react-native-safe-area-context';
3+
import Navigation from './navigation';
4+
import { PlayerProvider } from './contexts/PlayerContext';
5+
import { useSetupAudioPro } from './hooks/useSetupAudio';
6+
7+
function AppContent() {
8+
useSetupAudioPro();
39

4-
export default function App() {
510
return (
611
<>
712
<StatusBar barStyle="dark-content" />
8-
<View style={styles.container}>
9-
<Text style={styles.title}>React Native Music Library</Text>
10-
11-
<TrackList />
12-
</View>
13+
<Navigation />
1314
</>
1415
);
1516
}
1617

17-
const styles = StyleSheet.create({
18-
container: {
19-
flex: 1,
20-
alignItems: 'center',
21-
justifyContent: 'center',
22-
paddingVertical: 40,
23-
paddingHorizontal: 20,
24-
backgroundColor: '#f5f5f5',
25-
},
26-
title: {
27-
fontSize: 24,
28-
fontWeight: 'bold',
29-
marginBottom: 10,
30-
textAlign: 'center',
31-
},
32-
});
18+
export default function App() {
19+
return (
20+
<SafeAreaProvider>
21+
<PlayerProvider>
22+
<AppContent />
23+
</PlayerProvider>
24+
</SafeAreaProvider>
25+
);
26+
}
375 KB
Loading
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, { createContext, useContext, useState, useCallback } from 'react';
2+
import type { Track } from '../../../src/NativeMusicLibrary';
3+
import type { AudioProTrack } from 'react-native-audio-pro';
4+
import { AudioPro } from 'react-native-audio-pro';
5+
6+
interface PlayerContextType {
7+
playlist: Track[];
8+
setPlaylist: (tracks: Track[]) => void;
9+
playingTrack: Track | null;
10+
setPlayingTrack: (track: Track) => void;
11+
playNext: () => void;
12+
playPrevious: () => void;
13+
}
14+
15+
const PlayerContext = createContext<PlayerContextType | undefined>(undefined);
16+
17+
export function PlayerProvider({ children }: { children: React.ReactNode }) {
18+
const [playlist, setPlaylist] = useState<Track[]>([]);
19+
const [playingTrack, setPlayingTrack] = useState<Track | null>(null);
20+
21+
const playNext = useCallback(() => {
22+
if (!playingTrack || playlist.length === 0) return;
23+
const currentIndex = playlist.findIndex(
24+
(track) => track.id === playingTrack.id
25+
);
26+
const nextIndex = (currentIndex + 1) % playlist.length;
27+
const nextTrack = playlist[nextIndex];
28+
if (nextTrack) {
29+
AudioPro.play(nextTrack as unknown as AudioProTrack);
30+
setPlayingTrack(nextTrack);
31+
}
32+
}, [playingTrack, playlist]);
33+
34+
const playPrevious = useCallback(() => {
35+
if (!playingTrack || playlist.length === 0) return;
36+
const currentIndex = playlist.findIndex(
37+
(track) => track.id === playingTrack.id
38+
);
39+
const prevIndex = (currentIndex - 1 + playlist.length) % playlist.length;
40+
const prevTrack = playlist[prevIndex];
41+
if (prevTrack) {
42+
AudioPro.play(prevTrack as unknown as AudioProTrack);
43+
setPlayingTrack(prevTrack);
44+
}
45+
}, [playingTrack, playlist]);
46+
47+
return (
48+
<PlayerContext.Provider
49+
value={{
50+
playlist,
51+
setPlaylist,
52+
playNext,
53+
playPrevious,
54+
playingTrack,
55+
setPlayingTrack,
56+
}}
57+
>
58+
{children}
59+
</PlayerContext.Provider>
60+
);
61+
}
62+
63+
export function usePlayer() {
64+
const context = useContext(PlayerContext);
65+
if (context === undefined) {
66+
throw new Error('usePlayer must be used within a PlayerProvider');
67+
}
68+
return context;
69+
}

example/src/hooks/useSetupAudio.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import type { AudioProEvent, AudioProTrack } from 'react-native-audio-pro';
2+
import { useEffect } from 'react';
3+
import {
4+
AudioPro,
5+
AudioProContentType,
6+
AudioProEventType,
7+
} from 'react-native-audio-pro';
8+
import { usePlayer } from '../contexts/PlayerContext';
9+
10+
type TrackCallback = () => {
11+
currentTrackId: string | null;
12+
playList: AudioProTrack[];
13+
};
14+
15+
let getTrackStateCallback: TrackCallback | null = null;
16+
17+
// setup audio pro outside react native lifecycle
18+
export function setupAudioPro(): void {
19+
// Configure audio settings
20+
AudioPro.configure({
21+
contentType: AudioProContentType.MUSIC,
22+
debug: true,
23+
debugIncludesProgress: false,
24+
progressIntervalMs: 1000,
25+
});
26+
27+
// Set up event listeners that persist for the app's lifetime
28+
AudioPro.addEventListener((event: AudioProEvent) => {
29+
if (!getTrackStateCallback) return;
30+
31+
switch (event.type) {
32+
case AudioProEventType.TRACK_ENDED:
33+
// Auto-play next track when current track ends
34+
playNextTrack(getTrackStateCallback);
35+
break;
36+
37+
case AudioProEventType.REMOTE_NEXT:
38+
// Handle next button press from lock screen/notification
39+
playNextTrack(getTrackStateCallback);
40+
break;
41+
42+
case AudioProEventType.REMOTE_PREV:
43+
// Handle previous button press from lock screen/notification
44+
playPreviousTrack(getTrackStateCallback);
45+
break;
46+
47+
case AudioProEventType.PLAYBACK_ERROR:
48+
console.warn('Playback error:', event.payload?.error);
49+
break;
50+
}
51+
});
52+
}
53+
54+
function playNextTrack(
55+
getTrackState: TrackCallback,
56+
autoPlay: boolean = true
57+
): void {
58+
const { currentTrackId, playList } = getTrackState();
59+
60+
if (playList.length === 0) return;
61+
62+
const currentIndex = playList.findIndex(
63+
(track) => track.id === currentTrackId
64+
);
65+
const nextIndex = (currentIndex + 1) % playList.length;
66+
const nextTrack = playList[nextIndex];
67+
68+
AudioPro.play(nextTrack!, { autoPlay });
69+
}
70+
71+
function playPreviousTrack(
72+
getTrackState: TrackCallback,
73+
autoPlay: boolean = true
74+
): void {
75+
const { currentTrackId, playList } = getTrackState();
76+
77+
if (playList.length === 0) return;
78+
79+
const currentIndex = playList.findIndex(
80+
(track) => track.id === currentTrackId
81+
);
82+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : playList.length - 1;
83+
const prevTrack = playList[prevIndex];
84+
85+
AudioPro.play(prevTrack!, { autoPlay });
86+
}
87+
88+
// setup audio pro inside react native lifecycle
89+
export function useSetupAudioPro(): void {
90+
const { playlist, playingTrack } = usePlayer();
91+
92+
// Update the callback function
93+
useEffect(() => {
94+
getTrackStateCallback = () => ({
95+
currentTrackId: playingTrack?.id ?? null,
96+
playList: playlist as unknown as AudioProTrack[],
97+
});
98+
}, [playlist, playingTrack]);
99+
}
100+
101+
export function getProgressInterval(): number {
102+
return AudioPro.getProgressInterval()!;
103+
}
104+
105+
export function setProgressInterval(ms: number): void {
106+
AudioPro.setProgressInterval(ms);
107+
}

example/src/navigation/index.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { NavigationContainer } from '@react-navigation/native';
2+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
3+
import IndexScreen from '../pages/IndexScreen';
4+
import PlayerScreen from '../pages/PlayerScreen';
5+
6+
export type RootStackParamList = {
7+
Index: undefined;
8+
Player: undefined;
9+
};
10+
11+
const Stack = createNativeStackNavigator<RootStackParamList>();
12+
13+
export default function Navigation() {
14+
return (
15+
<NavigationContainer>
16+
<Stack.Navigator>
17+
<Stack.Screen
18+
name="Index"
19+
component={IndexScreen}
20+
options={{ headerShown: false }}
21+
/>
22+
<Stack.Screen
23+
name="Player"
24+
component={PlayerScreen}
25+
options={{
26+
title: '正在播放',
27+
headerBackTitle: '返回',
28+
}}
29+
/>
30+
</Stack.Navigator>
31+
</NavigationContainer>
32+
);
33+
}
Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,28 @@ import {
66
Text,
77
StyleSheet,
88
Image,
9+
TouchableOpacity,
910
} from 'react-native';
1011
import { getTracksAsync } from '@nodefinity/react-native-music-library';
1112
import { useState } from 'react';
12-
import type { AssetsOptions, Track } from '../../src/NativeMusicLibrary';
13-
import { usePermission } from './usePermission';
13+
import type {
14+
AssetsOptions,
15+
Track,
16+
} from '@nodefinity/react-native-music-library';
17+
import { usePermission } from '../hooks/usePermission';
1418
import { pickDirectory } from '@react-native-documents/picker';
19+
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
20+
import type { RootStackParamList } from '../navigation';
21+
import { SafeAreaView } from 'react-native-safe-area-context';
22+
import { usePlayer } from '../contexts/PlayerContext';
1523

16-
export default function TrackList() {
24+
type Props = NativeStackScreenProps<RootStackParamList, 'Index'>;
25+
26+
export default function IndexScreen({ navigation }: Props) {
1727
const [tracks, setTracks] = useState<Track[]>([]);
1828
const [loading, setLoading] = useState(false);
1929
const { permissionStatus, requestPermissions } = usePermission();
30+
const { setPlaylist, setPlayingTrack } = usePlayer();
2031

2132
const getAllTracks = async () => {
2233
try {
@@ -77,8 +88,26 @@ export default function TrackList() {
7788
};
7889

7990
const renderItem = ({ item }: { item: Track }) => (
80-
<View style={styles.trackItem}>
81-
<Image source={{ uri: item.artwork }} style={styles.cover} />
91+
<TouchableOpacity
92+
style={styles.trackItem}
93+
onPress={() => {
94+
setPlaylist(tracks);
95+
setPlayingTrack(item);
96+
console.log('item', item);
97+
navigation.navigate('Player');
98+
}}
99+
>
100+
<Image
101+
source={
102+
item.artwork
103+
? {
104+
uri: item.artwork,
105+
}
106+
: require('../assets/default_artwork.png')
107+
}
108+
style={styles.cover}
109+
defaultSource={require('../assets/default_artwork.png')}
110+
/>
82111
<View style={styles.trackInfo}>
83112
<Text style={styles.trackTitle} numberOfLines={1}>
84113
{item.title}
@@ -88,7 +117,7 @@ export default function TrackList() {
88117
</Text>
89118
</View>
90119
<Text style={styles.duration}>{formatDuration(item.duration)}</Text>
91-
</View>
120+
</TouchableOpacity>
92121
);
93122

94123
const getTracksFromPickedDirectory = async () => {
@@ -113,7 +142,8 @@ export default function TrackList() {
113142
};
114143

115144
return (
116-
<>
145+
<SafeAreaView style={styles.container}>
146+
<Text style={styles.title}>React Native Music Library</Text>
117147
<View style={styles.buttonContainer}>
118148
<Button
119149
title={`${loading ? 'loading...' : ''} get all tracks`}
@@ -136,14 +166,23 @@ export default function TrackList() {
136166
style={styles.list}
137167
/>
138168
)}
139-
</>
169+
</SafeAreaView>
140170
);
141171
}
142172

143173
const styles = StyleSheet.create({
174+
container: {
175+
flex: 1,
176+
paddingHorizontal: 20,
177+
},
178+
title: {
179+
fontSize: 24,
180+
fontWeight: 'bold',
181+
marginBottom: 10,
182+
textAlign: 'center',
183+
},
144184
buttonContainer: {
145185
width: '100%',
146-
maxWidth: 300,
147186
gap: 10,
148187
},
149188
list: {

0 commit comments

Comments
 (0)